UVM Run-Time Phasing Guide
UVM Run-Time Phasing Guide
Abstract
Since the release of the UVM 1.0, one of the least documented features of the
methodology has been the Run-Time Phasing solution. Due to this lack of documen-
tation, many users were immediately turned off from trying to use this new area of
the methodology. Even more unfortunate were those users who were brave enough
to try and blaze the trail, but were quickly mired down in misuse, misinterpretation,
and a general lack of support. This lack of documentation, combined with a general
misunderstanding of what Run-time phasing was intended to solve, lead to many users
labeling it as “unsafe”, and “overly complicated.”
This document strives to remove the veil of confusion which the UVM’s Run-time
phasing is wrapped in, by clarifying its intent and showing how easy it is to build very
powerful stimulus.
Copyright○
c
2013 NVIDIA CORPORATION, All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file
except in compliance with the License. You may obtain a copy of the License at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the
License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific language governing
permissions and limitations under the License.
Notices
While this guide offers a set of instructions to perform one or more specific verification tasks,
it should be supplemented by education, experience and professional judgment. Not all
aspects of this guide may be applicable in all circumstances.
This guide has not been approved through the Accellera consensus process, and serves only
to increase the awareness of information and approaches available within the Universal Ver-
ification Methodology.
The UVM 1.1 Class Reference represents the foundation used to create this document. This
document is a way to apply the UVM 1.1 Class Reference, but it is not the only way. This
document has been contributed with the belief that standards remain an important ingredient
in fostering innovation within the industry.
The UVM 1.1 Class Reference and the UVM 1.1 User’s Guide can be found on the Accellera
VIP Working Group’s current website:
http://www.accellera.org/activities/vip
Contents
Contents i
1 Introduction 1
1.1 What is Run-Time Phasing? . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 What isn’t Run-Time Phasing? . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2.1 Intent of Stimulus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2.2 DUT Status . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 What About Virtual Sequences? . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 High-Level Concepts 5
2.1 Phases and Schedules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.1 The Library Implementation of Phase Types . . . . . . . . . . . . . . 5
2.2 Run-time vs. Common Domains . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2.1 The Library Domain Implementation . . . . . . . . . . . . . . . . . . 7
2.3 Interaction Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.1 Phasing Awareness . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.2 Phasing Participation . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.4 Lifetime of a Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.4.1 Before a Phase’s Execution . . . . . . . . . . . . . . . . . . . . . . . . 10
2.4.2 During a Phase’s Execution . . . . . . . . . . . . . . . . . . . . . . . 10
2.4.3 After a Phase’s Execution . . . . . . . . . . . . . . . . . . . . . . . . 11
2.5 The Objection Mechanism . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3 Building A Schedule 13
3.1 Phase Execution Tasks and Functions . . . . . . . . . . . . . . . . . . . . . . 13
3.2 Our Simple Schedule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.3 Extending Our Schedule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.3.1 Injecting Serial Phases . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.3.2 Injecting Parallel Phases . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.3.3 Specific Locations Within A Parallel Schedule . . . . . . . . . . . . . 17
3.3.4 Injecting Phases With . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.4 Our Domain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.5 Synchronizing Domains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.5.1 Synchronize Everything Common . . . . . . . . . . . . . . . . . . . . 21
3.5.2 Synchronize A Single Phase . . . . . . . . . . . . . . . . . . . . . . . 22
3.5.3 Unsynchronizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.6 When to create Schedules and Phases . . . . . . . . . . . . . . . . . . . . . . 23
3.6.1 When not to create a phase . . . . . . . . . . . . . . . . . . . . . . . 23
3.6.2 When to safely create a phase . . . . . . . . . . . . . . . . . . . . . . 24
3.7 Who should create Schedules and Phases . . . . . . . . . . . . . . . . . . . . 24
i
4.1 “Always Objecting” Sequence . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.1.1 Why not use pre_body and post_body? . . . . . . . . . . . . . . . . 27
4.1.2 Why the null check? . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.1.3 A final note on do_kill . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.2 “Sometimes Objecting” Sequences . . . . . . . . . . . . . . . . . . . . . . . . 28
4.3 “Background” Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.4 “State-Based” Sequence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
4.5 “Phase Time” Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.5.1 Minimum Execution Time . . . . . . . . . . . . . . . . . . . . . . . . 35
4.5.2 Maximum Execution Time . . . . . . . . . . . . . . . . . . . . . . . . 36
4.6 Connecting Stimulus To Our Schedules . . . . . . . . . . . . . . . . . . . . . 37
4.6.1 Starting a Phase’s Default Sequence . . . . . . . . . . . . . . . . . . . 37
4.6.2 Starting More Than One Sequence . . . . . . . . . . . . . . . . . . . 38
4.6.3 A Word On Domains . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.6.4 Default Sequences and Phase Ended . . . . . . . . . . . . . . . . . . 39
6 Proposals 46
6.1 Zero-time Raise/Drop in STARTED . . . . . . . . . . . . . . . . . . . . . . . 46
6.2 Remove Unused Objections . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
6.3 Make Phases Non-Virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
6.4 Schedule Introspection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.5 Sync To Common Run . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.6 Mechanism to Object Automatically . . . . . . . . . . . . . . . . . . . . . . 47
6.7 Harden Starting Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.8 Inherit Starting Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.9 Provide Phase Objection Count . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.10 Example of Stimulus/Analysis Connection . . . . . . . . . . . . . . . . . . . 49
6.11 Contribution: Reusable Delay and Timeout Sequences . . . . . . . . . . . . . 50
6.12 Avoid Declaring Min/Max Times in Phase API . . . . . . . . . . . . . . . . 50
6.13 Default Sequence By Type Name . . . . . . . . . . . . . . . . . . . . . . . . 50
6.14 Contribution: Sequence To Start Children in Parallel . . . . . . . . . . . . . 50
6.15 Remove Single-Domain Restriction . . . . . . . . . . . . . . . . . . . . . . . 51
ii
1 Introduction
Before we go diving into code examples and whatnot, it would probably be a good idea to
make sure that everyone is on the same page with regards to what Run-Time Phasing is, and
just as importantly, what it is not.
In a nutshell, Run-Time Phasing is the UVM mechanism for controlling the flow and execution
of the test. It provides a standardized mechanism for synchronizing the various portions of
the test which may affect the overall flow of the test.
Prior to the UVM Run-Time Phasing solution, test flow was generally handled by writing
layers of ad-hoc sequences and virtual sequences, leaving it up to the integrator to determine
how (and even if ) stimulus from various sources could be synchronized. VIP Developer A
may have created a sequence which used events to indicate a flow, Developer B may have
written their sequence to expect a barrier to be provided which was used to synchronize with
the outside, and Developer C may have chosen a completely different mechanism.
The UVM Run-Time Phasing solution provides a standardized mechanism for expressing test
flow, and simultaneously provides a standardized mechanism for synchronizing within that
test flow.
The list of things which the UVM Run-Time Phasing solution is not intended to solve is
boundless, however there are some items which seem to come up time and again. In this
section we’ll outline these common misinterpretations, and detail why Run-Time phasing
shouldn’t be used for them.
You’ll notice that nowhere in the definition is “intent” mentioned. The UVM’s Run-Time
Phasing solution is not designed to provide a mechanism for qualifying what the intent of a
given stimulus developer was. Instead, it simply provides a standardized mechanism for that
developer to declare that they are, or are not, ready for the test to proceed.
Let’s take a look at a brief example to clarify this point: For this example, we’ll say we have
a sequence which is responsible for training a PCIe device.
Our first inclination may be to say that the sequence should be written to wait for the
configure_phase, and then execute the required stimulus, but this would be very bad.
What if an integrator wants to run our sequence during the main_phase? What if they want
to put us in a schedule we weren’t even aware of when writing the sequence?
1
Our problem here is that we fell in to a common trap. . . we tried to map the intent of our
sequence onto a particular phase of the test. As our VIP sequence writers are rarely our test
writers, it is unlikely that we’d be able to successfully align our sequence with the various
phases of the test flow.
Instead of trying to build a connection between the sequence and a specific phase of the test,
we should simply write our sequence to be aware of the fact that it was launched during a
phase of the test. When the sequence is launched, it can prolong the phase until training
is complete, and then it can stop prolonging the phase. If the test writer wants to run
our sequence during the configure_phase, that’s fine. . . if they want to run it again during
the main_phase, that’s also fine. Heck, if they wanted to completely bypass the Run-Time
Schedule, and run our sequence during the run_phase, no harm, no foul. . . all is good in the
world.
Another big trap that many developers fall into has to do with the difference between “Test
Phases” and “DUT States.” The most obvious example of this confusion is the UVM’s
reset_phase.
The UVM’s reset_phase is quite possibly the most unfortunately named piece of the entire
methodology. Is the intention that the DUT should go in to reset during that phase? Is the
intention that whenever the DUT is in reset, that phase is active? Should the stimulus be
driving the DUT to reset in that phase, or should the stimulus have already driven the DUT
to reset before that phase begins?
Generally speaking, there should be no hard-implied connection between the DUT being in
reset, and the reset_phase being active or inactive. In order to provide clarity between
these two concepts, we will be using two separate terms:
Type Definition
STATE State is used to describe the status of something. The DUT could be in the ‘reset’
state at any time... regardless of what portion of the test flow we are in.
PHASE Phases are used by the test writer to control the flow of the test. One could say
that the current active phase(s) are the ‘state’ of the test flow.
With these two definitions, we can now begin answering (and clarifying) our questions from
before:
∙ Is the intention that the DUT should go in to reset during that phase?
This is up to the test writer. If they wish, they can start stimulus in the reset phase
which will place the DUT into the reset state.
∙ Is intention that whenever the DUT is in reset, that phase is active?
No. When the DUT is in the reset state, any given phase could be active.
2
∙ Should stimulus be driving the DUT to reset in that phase, or should the stimulus have
already driven the DUT to reset before the phase begins?
Again, this is the test writer’s discretion. They may choose to execute stimulus which
puts the DUT into the reset state in the reset phase, or they may choose to do so in
a different phase. . . there is no hard connection between the DUT being in the reset
state, and any particular phase being active.
Many users look at the UVM’s Run-Time Phasing solution and say "Why would we need
that, why can’t we simply use virtual sequences?" In this section we’re going to lay out what
you can’t do with virtual sequences, and why the UVM’s Run-Time Phasing solution is so
useful.
As we discussed in the introduction, one of the largest problems before the UVM’s Run-Time
Phasing solution was that of common mechanisms for synchronization. There was no pre-
defined and agreed upon set of states for dictating how a given portion of the test flow was
supposed to proceed.
For example, in OVM a simple virtual sequence could be written which simultaneously forked
off three different test sequences. Each test sequence is intended to test a specific portion of
the DUT, and each sequence runs until it considers the test complete. When all three test
sequences returned, our virtual sequence exited its body() task.
Some would argue that that type of synchronization is the only type of synchronization which
is required, and that virtual sequences are therefore the be-all, end-all when it comes to test
flow. Unfortunately, that form of control is often far too restrictive for advanced testbenches
which require multiple levels of test integration, and even proves overly restrictive for simple
testbenches which are trying to execute multiple types of stimulus.
Using the virtual-sequence-only model, it’s very difficult for a sequence writer to write any
form of "background" or "secondary" stimulus. We’ll get into the details of exactly what those
are in section 4, but for right now we can sum them up as “Stimulus which wants to execute
only when other stimulus is executing” (think: noise generator). Our low level sequence
writer would need to come up with some form of handshake for the virtual sequence writer
to use when indicating “it’s time to send stimulus” and more importantly “it’s time to stop
sending stimulus.” If we’re integrating into a large environment, wherein we may have VIP
coming from any number of sources, the chances of all of those sources giving us compatible
handshakes drops significantly.
It also becomes very difficult to write virtual sequences which inject themselves mid-stream
into an existing virtual sequence’s flow. In other words, it’s very difficult for a test writer
to say “When virtual sequence ‘A’ gets to a certain point in its flow, we want it to stop
processing so that we can do something else. . . then we want it to start up again when we’re
done.”
This form of handshaking and schedule manipulation is handled quite nicely by the UVM
3
Run-Time Phasing solution, and scales from small single-block testbenches up to very large
SoC integration testbenches.
Does this mean that users should abandon the concept of using virtual sequences entirely?
Absolutely not! The virtual sequence is still an enormously powerful mechanism for launching
many sequences together within a single phase of the schedule. The UVM Run-Time Phasing
solution works together with virtual sequences to provide a standardized mechanism for
describing the flow of the schedule, and for describing the individual states of a single phase
of execution within that schedule.
4
2 High-Level Concepts
Now that we understand what the UVM’s Run-time Phasing solution was (and wasn’t)
intended to solve, we just need to work out some basic concepts before we get into example
code.
The UVM uses these two terms to describe the building blocks of testbench and test execution
and control of flow.
Type Description
PHASE A ‘phase’ is a period of the test flow which has a “beginning” and an “end”.
SCHEDULE A ‘schedule’ is a graph of interconnected phases. Schedules can be hierarchical
in nature, meaning that a parent schedule may contain a graph of intercon-
nected children schedules (and phases).
As currently implemented in the UVM, schedules execute in the form of a Directed Graph.
In some tutorials the schedules are referred to as a ‘DAG’, or Directed Acyclic Graph, but
this isn’t entirely accurate, as the UVM schedules support jumping backwards to repeat prior
parts of the schedule.
The UVM also provides a special form of schedule, known as a domain.
Type Description
DOMAIN A ‘domain’ is the highest level the schedule hierarchy. Unlike a normal schedule,
domains should not be children of schedules. Domains also provide a point at
which notification boundaries exist. . . which is to say that we can request to be
notified about all phases, or all phases within a given set of domains.
Inside of the UVM Library, one basic construct is provided which implements phases, sched-
ules, and domains: the uvm_phase.
5
The uvm_phase class comes in many different flavors, or “types”:
Type Description
IMP A phase implementation (or ‘IMP’) is the lowest level building block that
the library provides. The IMP is responsible for handling the phase-specific
callbacks (if any).
NODE A phase node represents a single node instance in the graph. Nodes are
required internally to the library, as a single phase IMP may be inserted into
the graph at multiple locations. While each location shares the same IMP
type, they each have separate NODE instances. Additionally, the NODE is
what will be passed in to the phase callbacks.
SCHEDULE The schedule type represents an encapsulated portion of the phasing graph.
These typically consist of several NODE types, in series, parallel, or both.
TERMINAL An internal object which serves as the termination NODE for a SCHEDULE
phase object.
DOMAIN This object represents the entire graph segment that executes in parallel with
the run_phase. Like schedules, domains typically consist of several NODE
types.
Of all of those types, the only one which gets its own class definition is the “Domain” type.
The uvm_domain class extends the uvm_phase class. For all other versions, the base class is
used by passing the type to the constructor.
One area of the UVM’s Phasing solution which has caused some confusion is the difference
between the “Run-Time Domains”, and the “Common Domain”. While both are performing
the same exact type of functionality, they’re doing so for slightly different purposes.
Domain Description
Common These phases are intended to synchronize the construction, configuration, and
eventual shutdown of the testbench.
Run-Time These phases are intended to synchronize the flow and execution of a test.
This is a very subtle difference, and in reality the underlying implementation of the Common
and Run-time phases in the UVM is the same. One way to think about it would be like this:
Common phases exist so that you can instance and configure your compo-
nents, connect TLM ports, and then verify/report the status of the DUT when
the simulation is ending. Generally speaking, the common phases are identical
throughout all testbenches and all tests. They will always run, and everyone
knows what they all are in advance.
6
Run-time phases are more dynamic in nature, and can change on a test-by-test
basis. A VIP developer should never presume to know what all of the Run-time
phases are going to be, as different environments and different integration levels
may have different schedule requirements.
The UVM library provides the “Common” domain, and a single run-time domain, called the
“UVM” domain. The “Common” domain has the following schedule:
build_phase
connect_phase
end_of_elaboration_phase
start_of_simulation_phase
run_phase
extract_phase
check_phase
report_phase
final_phase
NOTE: To those of you who are coming from the OVM, the UVM’s “Common” domain
should look exceedingly familiar, as it is basically just the OVM’s phasing schedule.
As we said though, the “Common” domain is primarily intended to be used for testbench
creation, and can be generally ignored when discussing Run-Time Phases. The only really
important part of the “Common Domain”, from a test flow/execution point of view, is the
run_phase. That phase is generally responsible for consuming all time during a simulation.
If you’re moving a clock forward, you should be in the run_phase.
The other domain provided by the UVM is the oddly named “UVM” domain. We say that
it’s oddly named because all domains are “UVM” domains. . . even the “Common Domain”
is technically a “UVM” domain. Oh well, just one more oddly named feature in the UVM’s
Run-Time phasing solution.
7
The “UVM” Domain is the domain for the Run-Time phases which the UVM provides by
default. As these phases are intended to control the flow of the test, and the test (probably)
consumes time, they all execute during the run_phase from the “Common” Domain.
The “UVM” domain’s schedule looks like this:
start_of_simulation_phase
pre_reset_phase
reset_phase
post_reset_phase
pre_configure_phase
configure_phase
post_configure_phase
run_phase
pre_main_phase
main_phase
post_main_phase
pre_shutdown_phase
shutdown_phase
post_shutdown_phase
extract_phase
Notice that the “UVM” Domain and the run_phase from the “Common” Domain are syn-
chronized. This means that run_phase and pre_reset_phase will start simultaneously. Ad-
ditionally, the UVM phasing schedule does not allow for "dead time", or time during which
a phase has ended, but its successor is not yet starting. This means that neither run_phase
nor post_shutdown_phase will end until both are ready to proceed.
8
2.3 Interaction Models
When discussing the UVM’s Run-Time phasing mechanism, it helps to define the list of pos-
sible interaction models. These models can generally be separated into two distinct groups:
Awareness and Participation.
Once you’ve made the decision that your stimulus should be aware of the Run-Time phasing
mechanism, then it will generally fit into one of two groups:
Type Description
PHASE The first group of stimulus is “Phase Aware,” which is to say that it is aware
of the fact that it is executing within a single phase. Most sequences which
we discuss will fall into this group.
SCHEDULE The second group is a bit more advanced, and that is “Schedule Aware.”
Schedule aware stimulus is aware of the expected flow of the test, and may do
things to actively change that flow (such as inserting new phases, or jumping
within the schedule).
The general rule of thumb for determining which group your code falls in to is this: If your
code is aware of the fact that there is more than one phase in the test, then it is “Schedule
aware.”
In addition to the two “Awareness” groups, stimulus will also fall into the following categories:
Type Description
ACTIVE Those participants which intend to manipulate the flow of the test, are said to
be active.
PASSIVE Those participants which are informed of changes in the flow of the test, but
which do nothing to change the flow of the test, are said to be passive.
NON Those who do nothing to manipulate the flow of the test, and are not informed
about changes in the flow of the test, are said to be non-participants.
One important note about the three categories of participation is that they’re relative in
nature. A class may choose to be an ’active’ participant in one phase, while completely
ignoring other phases (ie. being a ’non-participant’ in those phases).
The participation categories defined in this section can be combined with the “Awareness”
groups discussed in the previous section to create a complete description of a given phasing
interaction model. For example: An “Active, Phase-Aware” sequence understands that it is
9
running during a given phase, and will do something to either prolong that phase, or end it
prematurely.
In section 2.1, we described the UVM’s phases as having a “Beginning” and an “End.” In
reality, the UVM actually provides many specific states which describe the entire lifetime of
a phase. In these next few sections, we’ll describe these states.
Prior to a Phase being started, the phase will transition through three states:
State Description
DORMANT A phase is ’dormant’ when it hasn’t been run yet, and the Library is not
working on starting the phase.
SCHEDULED Once the Library has determined that a phase should be started, the phase
is moved into the ’scheduled’ state. It will stay in this state until all of the
predecessors have completed.
SYNCING The final step before a phase can be started is to synchronize with any other
phases which are starting at the same time (This is described in more detail
in section 3).
The latter two states are primarily used for internal synchronization within the Library itself,
and they will go by in rapid succession. The only reason we’re even discussing them here is
because they’re called out in the Reference Guide.
While a Phase is executing, the UVM defines four potential states for the Phase to be in.
The first state of a Phase’s execution is:
State Description
STARTED When a phase is ’started’, the Library is going to begin processing the actual
execution of the phase.
10
Once a phase has been ’started’, but before the Library is going to end it, the UVM provides
two potential states for the phase to be in:
State Description
EXECUTING A phase is in the ’executing’ state so long as any active participants
wish to prolong it.
READY TO END When all of the active participants are no longer attempting to prolong
the phase, then it moves into the ’ready to end’ state. This state is
basically a "chicken switch". . . you have one last chance to prolong the
phase before the phase ends.
In section 2.2.1, we described how the library does not allow any parallel phases which share a
common successor to end, until all of those phases are ready to end. This has the consequence
of making all such phases stay in the ‘executing’ state until ALL active participants in those
phases are willing to move forward.
For example: since the UVM’s run_phase and post_shutdown_phase have a common suc-
cessor (the extract_phase), neither phase will exit the ‘executing’ state until both are ready
to proceed.
When no participants remain who wish to prolong the phase, then the it moves into its final
state of execution:
State Description
ENDED When a phase transitions to the ’ended’ state, the Library is going to begin pro-
cessing the termination of the phase. Active participants no longer have the ability
to prolong the phase (we have passed the point of no return).
One final item to note is that the function-based phases, like the build_phase, can never
be prolonged. As such, they transition directly through STARTED, EXECUTING, and
ENDING, and skip READY TO END entirely.
Once the UVM Library has moved forward to end the phase, there are two final states that
the phase will transition through:
State Description
CLEANUP When a phase is in the ’cleanup’ state, all tasks associated with the phase are
killed, and any remaining objections are cleared.
DONE A phase is ’done’ after all execution related to that phase has been completed.
11
2.5 The Objection Mechanism
For time consuming phases, the user requires a mechanism for expressing that they are, or
are not, ready for the phase to end. The UVM’s Phases provide this mechanism in the form
of the phase_done objection.
An objection is a mechanism for multiple threads to independently declare that they want
to prevent something from happening. In this case, that something is the progression of the
phase to the “ENDED” state. If a participant wants to prolong a phase’s execution, then
they raise an objection to the phase ending. If they no longer wish to prolong the phase,
then they drop their objection to the phase ending. When all objections have been dropped,
the phase moves to the next state.
NOTE: In the past, the objection system in the UVM has gotten a bad rap, namely for
being implemented in a very inefficient way. With the introduction of the UVM
1.1d, many of the inefficiencies of the objection system have been completely re-
factored, allowing for a significant performance boost.
A final note regarding the objections and the UVM’s Run-time phasing: Since participation in
any given Run-time phase is optional (including the run_phase in the “Common” Domain),
the phasing system must assume that if there are no participants at the beginning of a phase,
then the phase is unused, and should be skipped. Because of this rule, participants must
raise their objection within one non-blocking assignment region of the phase changing to the
“STARTED” state. If your test appears to be ending in zero time, it is probably because
something isn’t raising its objection fast enough.
Please refer to Proposals 6.1 and 6.2 for potential areas where the usage of objections within
the UVM Run-Time Phases can be enhanced.
12
3 Building A Schedule
The UVM provides a default schedule which is present in all tests. This schedule was created
with the intention of providing some common names which everyone would understand. Un-
fortunately, while everyone understood the names which were used, they also had completely
different interpretations of how they were meant to be used.
In order to avoid this confusion, and in order to illustrate just how easy it is to create a
simple schedule, we’re going to ignore the UVM’s built-in run-time schedule in this section.
NOTE: Just because we’re ignoring the UVM’s run-time schedule, that doesn’t mean it’s
‘bad’ and should never be used. If it works for your purposes, then by all means feel
free to use it. . . it’s just worth acknowledging that what one group thinks should
happen in a ‘configure’ phase could be entirely different than what another group
thinks.
The phases built in to the UVM have specific tasks and functions which they intend to exe-
cute inside of components every time they are active (like build_phase() and run_phase().
While this makes perfect sense for the common phases, it doesn’t particularly make sense for
the run-time phases.
Unfortunately, the implementation of uvm_phase came from before the run-time phases even
existed, and because of that the documentation isn’t very straight forward on how to create
a phase which doesn’t require those component-based methods, luckily we can create such a
phase with only a few lines of code:
. . . and that is all you need in order to produce a phase which doesn’t try and trigger any
13
phase-specific callback methods within your components. Additionally, we’ve implemented
our phase implementation as a singleton (per phase name). This allows us to save on memory,
so that the user isn’t creating many IMPs which have the same name.
NOTE: For the rest of this document, we’re going to use simple_task_phases to build our
schedules. This is not a requirement of the UVM, we just feel it makes life a little
easier.
Please refer to Proposal 6.3 for a potential simplification within the UVM Library which
could make declarations of user-defined Run-Time Phases easier.
Before we dive in to the actual creation of our schedule, we first need to define exactly what
we want our test flow and execution to look like.
Let’s say that our DUT requires a basic power-good/reset initialization. In order to avoid
the confusion that we discussed in section 1.2.2, we’re just going to call this Phase A.
After this phase of our stimulus has executed, our DUT needs to do some basic training
before the outside world can communicate with it. We’re going to call this Phase B of our
execution.
Once our test has cleared through the reset and training phases of our execution, we’re going
to want to execute our primary test goals. We’re going to call this Phase C.
Finally, once our test goals are complete, we have some basic cleanup stimulus which is going
to need to execute. This stimulus will dump the register state of the DUT, and make sure
all of the internally stored values are what we expect them to be. We’re going to call this
Phase D.
NOTE: In case it isn’t obvious, we’re bending over backwards so as to not use the same
names for our phases that the “UVM” Domain does. Unfortunately, by providing
12 “Run-Time Phases”, the UVM has effectively removed the words reset, configure,
main and shutdown from our usable vocabulary in this document.
When all is said and done, our basic schedule ends up looking like this:
14
The source code required to build such a schedule is surprisingly straight forward:
As you can see, there’s nothing too complicated about making a basic schedule! By default,
when you add a phase to a schedule, the UVM library is going to add it to the end of that
schedule.
Additionally, you can see on line 8 that we don’t need to tell the library that our_schedule is
a “schedule” type of phase. The UVM library assumes that if you’re instantiating a phase, and
you don’t say what type you want, then you probably want a schedule. The uvm_task_phase,
and the simple_task_phase which extends it, overrides this default behavior because it is
always going to be an “implementation” type.
In the previous section, we described a very simple schedule, which was completely serial, ‘A’
before ‘B’ before ‘C’. . . and so on. What happens if we want something a little more compli-
cated (and for that matter, why would we ever want something a little more complicated)?
Let’s say that our group wants to extend our basic schedule, by running another phase after
our primary stimulus (Phase ‘C’), and before our cleanup stimulus (Phase ‘D’). Keeping with
15
our reasonably generic names, we’re going to call this phase ‘X’.
Since our entire schedule was already created in section 3.2, simply calling add(x_phase)
isn’t an option, as that would put the phase after Phase ‘D’. The UVM Library provides
multiple optional arguments to the add() method though, two of which can help us here:
Method Description
.after_phase() Adds the new phase “after” a given phase in the schedule. Any successors
to the given phase are now successors to the new phase, and the new
phase is the only successor to given phase.
.before_phase() Adds the phase “before” a given phase in the schedule. Any predecessors
to the given phase are now predecessors to the new phase, and the new
phase is the only predecessor to the given phase.
Let’s go ahead and use one of these methods to inject our new phase:
On line 9, you can see us passing in phase_c to the optional after_phase argument. By doing
this, we’re telling the library that we want our phase to come immediately after phase_c.
Since the only phase after “C” is “D”, and the only thing before “D” is “C”, we could have
just as easily said before_phase(phase_d) and achieved the same result. We’ll revisit this
topic again in section 3.3.3, once our schedule is a bit more complicated
Now that we have multiple phases going on between Phases “B” and “D”, we may run into
a bit of a dilemma. What if we want a sequence to run the entire length of time from when
Phase “B” ends, until phase “D” starts? If we start that sequence in Phase “C”, then the
library will terminate it before Phase “X” starts. We’ll need an additional phase, which runs
from when “B” ends, until “D” is about to start, and we’ll call this new phase “Y”.
16
Once again, we can use before_phase and after_phase to solve our problem:
Phase Y
Now that we have parallel phases running, something very interesting is happening at the
point in our schedule where Phases “X”, “Y”, and “D” meet. If we zoom in a little bit, we
can see that there are three places where a new phase could go:
Phase X 1
3
Phase D
Phase Y
2
We would see a similar (albeit inverted) diagram at the connection between Phases “B”, “C”
and “Y”.
17
The numbered locations are:
1. If we inject a phase at this location, then the new phase starts when Phase “X” ends,
and ends at the same time as Phase “Y”.
2. If we inject a phase at this location, then the new phase starts when Phase “Y” ends,
and ends at the same time as Phase “X”.
3. If we inject a phase at this location, then the new phase starts when both Phases “X”
and “Y” are end.
It is not particularly difficult to hit each of these locations, we just need to know which one
we’re aiming for.
Using this form of direct targeting of locations, we can now add phases anywhere we want
within a schedule, regardless of whether or not it has serial and/or parallel phases of execution.
In addition to the after_phase and before_phase arguments to the add() method, the
library supports one additional optional argument for creating parallel phases.
Method Description
.with_phase() Adds the new phase “with” (or “parallel to”) a given phase in the schedule.
Any predecessors and successors of the given phase are also going to be
predecessors and successors to the new phase.
18
3.4 Our Domain
In the previous section, we created our schedule... but the UVM library doesn’t know what
to do with it. It doesn’t know when we want it to start, and it doesn’t know who we want
to be able to hear it. That’s where picking a domain comes in to play.
Which domain should our schedule go in? There’s an obvious wrong answer (the “Common”
Domain, which isn’t meant for Run-Time phases), but there could be a few right answers.
We could put our schedule into the “UVM” Domain, at which point everyone who sees
the “UVM” Domain would now see our schedule execute. If we choose to do that though,
everyone is going to get the various callbacks and whatnot caused by our schedule executing.
If we don’t want everyone to see our schedule execute, then we simply create our own domain,
which will be synchronized to the run_phase, just like the “UVM Domain”. Luckily, this is
also quite easy to do:
And again. . . nothing too complicated. The domain is really just like a parent schedule for
our_schedule (and inside the library, that’s exactly how the UVM will treat it). There is
one final little block of code which is necessary, and that is the code which is required to
synchronize our new domain with the run_phase in the “Common Domain”.
Keeping in mind that the UVM Library’s implementation treats schedules as fancy phases,
and domains as fancy schedules, the above code is actually reasonably straight forward.
19
Let’s go ahead and dissect it real quickly:
∙ On line 12, we’re just fetching the “Common” Domain. The domain is available at any
time during simulation, by calling uvm_domain::get_common_domain().
∙ On line 15, we’re finding the run_phase which is in the common domain
∙ On line 18, we’re calling add(), and passing in our domain as the phase to add
∙ On line 19, we’re telling it to add the phase with (ie. parallel to) the run_phase
Why do we use common_domain.find(uvm_run_phase::get()) to get our reference to the
run_phase? Well, it’s an advanced topic, but for right now we’ll just say that phases are
allowed to occur multiple times within multiple schedules (yes, even the run_phase!). Since
that can happen, we need to make sure we’re aligning it with the proper run_phase, specif-
ically the one from the “Common” Domain.
NOTE: Please don’t ever create a Run-Time phase called ‘run’ (or ‘build’, ‘connect’, etc.)!
While it’s technically possible, it would make debug extra confusing.
Please refer to Proposal 6.5 for a mechanism to simplify the user code required to synchronize
to the Common run_phase.
In all of the examples discussed so far, we’ve been operating under the assumption that we are
creating the schedule. As such, we were able to put new phases anywhere we wanted. . . but
what if we’re being given two separate schedules, and need them to operate in lock-step?
The UVM Run-Time Phasing solution does support this, in the form of “Domain Synchro-
nization”.
For our examples, we’re going to have two domains like the one described in section 3.4. Just
to make life interesting though, we’ll place an additional phase into the second domain, so
that the two schedules don’t look identical.
20
Domain 1
Phase Y
Domain 2
Phase Y
Phase Z
Since both of our domains are going to start and end at the same time (when the “Common”
Domain’s run_phase starts and ends), they’re already slightly synchronized. That being
said, each of the domains will process their own schedule independently of the others, so it’s
possible that “Domain1.PhaseD” is executing while “Domain2.PhaseA” is executing.
The most basic form of synchronization between domains is to simply synchronize all of the
phases that the domains have in common.
This can be achieved with a simple one-liner:
By synchronizing the two domains, we have effectively drawn dotted lines between all of
the phases. “Domain1.PhaseA” will now be treated as though it was running with “Do-
main2.PhaseA”. . . and so on for all of the shared phases.
Since “PhaseZ” doesn’t exist in “Domain1”, there’s no direct synchronization there to speak
of, however “PhaseZ” can cause “PhaseY” and “PhaseX” to be prolonged, and those phases
are synchronized. This means that an objection to “Domain2.PhaseZ” ending effectively
prolongs “Domain1.PhaseX” and “Domain.PhaseY” as well.
21
3.5.2 Synchronize A Single Phase
What if instead of synchronizing all of the shared phases, we simply wanted to synchronize
“PhaseC”? This would mean that each domain would process “PhaseA” independently, how-
ever they would get in to lock-step at the end of “PhaseB”, and then break out of lock-step
at the end of “PhaseC”.
NOTE: A common reason for synchronizing single phases like this would be for SoC config-
uration. It’s possible that there are different reset and training stimulus which can
be phased independently, however whole-chip configuration can’t be written until
all are complete. Once whole-chip configuration is written, then the stimulus phases
can once again diverge.
The UVM Library supports synchronizing a single phase by using the optional .phase()
argument in the sync() method:
Now our dotted lines between the two domains only exist for “PhaseC.” The two domains
will execute independently, however they will both start and end “PhaseC” at the same time.
The UVM Library also supports synchronizing different phases within two domains. This is
useful if our domains are not identical in construction, but share a common intention. For
example, if we were handed a domain with a “configure” phase, and another domain which
has an “init” phase, and we just wanted to execute them both simultaneously. This can be
achieved by using the optional .with_phase() argument in the sync() method:
3.5.3 Unsynchronizing
In addition to the sync() method, the UVM Library supports the idea of “unsynchronizing”
phases between two domains. Usually, this is used as a short-cut when we want to synchronize
most of the phases in two domains, but we want to leave a few unsynchronized.
22
For example, let’s say that we wanted our two domains to be full synchronized, starting at
“PhaseC” and going to the end of simulation. We could use the .phase() argument inside
of sync() to accomplish this manually:
On the other hand, we can use unsync() (which has the same optional arguments as sync())
to simplify the code a bit:
The two code examples accomplish the same goals, we just need to pick whichever seems
more efficient for our test goals.
Now that we understand how to create our schedules, the next obvious question is when
should we create our schedules? In this section we’ll cover the basics, and some of the rules
that govern schedule creation times.
Technically, you can always create a new phase node in the schedule. That being said, there
are certain times when it may be too late for that new phase to execute.
23
If you place a new phase into a schedule, and the schedule is currently executing at a location
in the graph:
∙ past the new phase’s location in the graph or
∙ parallel to the new phase’s location in the graph
. . . then the phase will not execute. This is because in both of these scenarios the UVM
Library’s phase state machine (the “Phaser”) is already past the point when the phase was
supposed to start. The only way to get these “too-late” phases to execute is to jump the
Phaser back to a previous state, which is discussed in section 5.2.2.
Since the UVM Library considers a Schedule to be a fancy Phase, the same basic rules apply
for Schedules. On the other hand, the UVM Library treats Run-Time Domains as a ‘special’
form of Schedule, which are always intended to execute parallel to the “Common” Domain’s
run_phase. As such, it isn’t safe to create a new domain any time after the run_phase has
started.
Now that we know when we shouldn’t create a new phase in the graph, we’re going to lay
down some simple guidelines for when we can safely create our schedules.
Since we know that phases won’t start if they’re injected into the schedule after they were
supposed start, and we know that all Run-Time Domains are supposed to start executing
when the run_phase in the “Common” Domain starts, we can safely say that all Run-Time
domains (and their associated schedules) should be initially created during any of the pre-run
phases:
∙ build_phase
∙ connect_phase
∙ end_of_elaboration_phase
∙ start_of_simulation_phase
Once the initial schedules have been created, then “Schedule Aware” stimulus can be written
which adds new phases into the graph dynamically. So long as this stimulus follows the rules
laid out in the previous section for avoiding “too-late” phase injections, these new ‘dynamic’
phases can be injected at any point in time after the initial schedule was created.
The final piece of the “Building A Schedule” puzzle is to determine who should be responsible
for actually declaring what the schedule is going to be. Many users want to see a concrete rule
like: “Only uvm_test derivatives should create a schedule,” but unfortunately the answer is
a bit more nebulous.
24
Generally speaking, you are safe to declare your Schedules and Phases inside of any code
which is responsible for defining the expected test flow. This could certainly be your
uvm_test, and for the majority of users it probably will be. However, it could just as easily
be from an environment which is responsible for executing its own stimulus independent of
what that which the uvm_test is launching.
The best “guidelines” we’ve been able to come up with are these:
∙ The Initial Phases/Schedule/Domains need to be created during the pre-run common
phases, therefore they generally need to be declared inside of a uvm_component. The
most logical components for this would be uvm_test and uvm_env.
∙ Dynamic Phases (as described in the previous section) can be created at any point in
time during simulation. This means that anywhere we would be consider safe to launch
a new sequence, we can consider it safe to create a dynamic phase.
Remember, these are just guidelines, not hard-and-fast rules.
25
4 Writing Phase Aware Stimulus
The absolute simplest phasing role for the UVM’s stimulus is that of the “non-participant”
(as described in section 2.3). This is stimulus which is completely unaware of the flow of the
test. For rather obvious reasons, we’re not going to focus heavily on that area of stimulus.
This section will detail out the various different kinds of “Phase Aware” sequences. We do
not expect this to be a complete list, so much as a list of the more common forms.
Many times, sequences simply want to “always object” to a phase ending. . . or in other words,
so long as the sequence is executing, the objection should be raised. The easiest way to do
this is to place the raises/drops inside of the pre_start and post_start callback methods:
You may have noticed that instead of just dropping the sequence’s objection in post_start,
we created another method to perform the cleanup operation. This is a safety measure, be-
cause it’s possible that our sequence could be killed. If our sequence is killed, then post_start
doesn’t execute, and instead the do_kill method is executed.
26
13 virtual task always_sequence::do_kill();
14 do_cleanup(1);
15 super.do_kill();
16 endtask : do_kill
17
18 virtual function void always_sequence::do_cleanup(bit killed);
19 if (starting_phase != null) begin
20 starting_phase.drop_objection(this,
21 $sformatf("sequence has %s",
22 (killed) ? "been killed" : "ended"));
23 endfunction : do_cleanup
Being as how this code is likely to apply to most of your phase aware sequences, it would
probably be beneficial to write it into a base class from which they can all extend. In future
code examples, this document will refer to always_sequence as though it was such a base.
Please refer to Proposal 6.6 for a mechanism which could simplify writing sequences which
“always object.”
The UVM provides two set of callbacks which will be triggered before and after the execution
of a sequence’s body() task, they are pre_start/post_start and pre_body/post_body. So
why choose one over the other?
The only real difference between the two mechanisms is when (and if ) they’re going to be
executed. The pre_start and post_start callbacks are called on every execution of the
sequence, regardless how that sequence is started. On the other hand, the pre_body and
post_body callbacks are (generally) only called on sequences which are started by calling
seq.start(...). If a sequence is started via the ‘uvm_do(...) macro (or its variants),
then those callbacks will not be executed.
Any time an integrator tries to control whether or not the pre/post portions of a sequence
are going to execute, they’re just begging for trouble. What if some resource needed to
be acquired before the body executed? What if some resource needed to be cleared after
the sequence executed? Now the integrator is taking responsible for those actions, which is
usually a bad idea (and a violation of the encapsulation provided by the sequence).
NOTE: In the past, it has been said that by placing the raise/drop code within the pre_body
and post_body calls, there is an efficiency gain, as sequences are likely to launch
children sequences via ‘uvm_do instead of seq.start(). However, with the ob-
jection performance fixes discussed in section 2.5, many of these gains are now
negligible. Add to that the fact that it’s perfectly legal to start a child sequence via
seq.start(), and now you just have a confusing recommendation.
27
4.1.2 Why the null check?
In the code example 13, you can see that we’re checking to determine if the starting_phase
variable is null before we raise the objection.
Our primary reason is just good old-fashioned coding safety. Currently, the only way that
starting_phase gets assigned is by using the “default_sequence” mechanism within the
sequencer (We’ll discuss this in more detail later). If a parent sequence starts a child, and
doesn’t set the starting_phase variable manually, then the child generate a null-pointer-
dereference error when they try and raise their objection.
Please refer to Proposals 6.7 and 6.8 for ways in which the starting_phase logic within the
library can be hardened and enhanced.
In code example 14, we showed how the user can use the do_kill method to drop an objection
if something is killing the sequence. This can happen because a parent sequence has decided
to do something else, or because the phasing system is “jumping” to a new phase.
Phase jumps are a slightly advanced topic, however it’s important to note here that just
because we’ve dropped our objection, that doesn’t mean that everything within the system
is stable. It’s possible that we may have deadlocked some resource or even the internals of
the UVM library itself, because we were killed when we had objected to the phase ending.
In order to protect ourselves from hitting one of these deadlocks, the “killer” and the sequence
need to have some sort of handshake such that the sequence can consume time while cleaning
up. This is often referred to as a “Soft Jump”, which is definitely an advanced use case (which
we discuss in section 5.3.1).
We’ve discussed sequences which always object to the phase ending, so the next natural
progression is to sequences which sometimes object to the phase ending. The most common
form of the “Sometimes” objector is the good old-fashioned reactive sequence.
Let’s say that we’ve got a simple DUT, which is a memory interface. Requests go in to the
DUT, and the DUT forwards them to memory. Our test will have a normal sequence which
is executing the primary test stimulus (a series of reads and writes). Our test will also have
a "reactive" sequence sitting on the other side of the DUT, producing completions to the
various memory requests which are forwarded.
As sequence writers, we’ve got a few options for how we write our reactive sequence:
∙ Write it as a purely passive sequence. Our sequence needs to be started at the right
time, but after that it doesn’t need to participate in phasing. The primary test goal
will prolong the phase until it gets its responses, no need for me to worry about it.
28
∙ Write it as a sometimes-active sequence. Our sequence needs to be started at the right
time, but it isn’t going to prolong the phase unless it’s got something to do.
While the first case may work for your given testbench, it has a minor flaw, wherein the
sequence writer is making an assumption about how the testbench is going to be created,
and how control will work. If a testbench developer comes along expecting the sequence to
participate in UVM phasing, then we’ve got a problem. By taking the second approach,
we’re significantly safer, as we’re not objecting all the time, but only when we think it is
important for the phase to not end.
NOTE: The test writer can always declare that our reactive sequence should never be
allowed to prolong the phase, by setting our starting_phase variable to null. . . but
they do so at their own peril! Preventing the phase from ending prematurely is
necessary to avoid the deadlocks discussed in section 4.1.3.
The nice thing about “sometimes active” sequences is that they can generally be cut up in
to an “always passive” sequence which launches “always active” sequences.
Here’s the example of our “always active” responder. It extends an always_sequence base
class which handles the objection for it, as discussed in section 4.1.
29
We can now use that sequence inside of an “always passive” reactive sequence.
1 class reactive_sequence
2 extends uvm_sequence#();
3
4 // Constructor, etc...
5
6 // Body Task
7 virtual task body();
8 my_request_type req;
9 specific_response rsp_seq;
10 forever begin
11 // This unblocks when a request is available.
12 wait_for_req(req);
13 ‘uvm_create_on(rsp_seq, p_sequencer)
14 rsp_seq.original_req = req;
15 rsp_seq.starting_phase = starting_phase;
16 ‘uvm_send(rsp_seq)
17 end
18 endtask : body
19 endclass : reactive_sequence
As you can see, the “always passive” reactive sequence is handing its starting_phase to the
child on line 26. The “always active” child will handle the objection automatically, and if no
response is being handled, then the reactive sequence is willing to let the phase end.
Unfortunately, sometimes you can’t cleanly separate your goals like that, and you need to
just write it out as a single sequence. This can certainly be achieved, and an example is
provided below, but it is much more complex for the end user to write.
1 class reactive_sequence_alt
2 extends uvm_sequence#();
3
4 ‘uvm_object_utils(reactive_sequence_alt)
5 ‘uvm_declare_p_sequencer(sequencer_type)
6
7 function new(string name="unnamed-my_reactive_sequence");
8 super.new(name);
9 endfunction : new
30
11 virtual function void do_kill();
12 if (starting_phase != null) begin
13 uvm_objection spo = starting_phase.get_objection();
14 spo.drop_objection(this,
15 "sequence has been killed",
16 spo.get_objection_count(this));
17 end
18 super.do_kill();
19 endfunction : do_kill
On lines 11-19, our do_kill method is a bit more complicated in this sequence, since it’s
possible for the sequence to be killed when the objection is raised, or when the objection is
not raised.
Please refer to Proposal 6.9 for a mechanism which could simplify retrieving the current
objection count for a phase.
. . . continuing with our code example:
The third type of phase aware sequence is another common form of stimulus: The “Back-
ground” Sequence.
Background sequences (or noise/static generators, etc.) are meant to run at the same time
as primary stimulus, but their goal is to promote randomness within the DUT. They don’t
31
directly contribute towards achieving the test goals, and as such they really shouldn’t be
involved in controlling the flow of the test.
It is possible however that the background sequence has put the DUT or testbench into a
state which would considered unstable if the phase were allowed to end. Our background
sequence is going to need to wait for all of the primary test goal sequences to complete, and
then prolong the active phase long enough to perform whatever cleanup may be necessary.
Since we’re not always prolonging the phase, that means that our background sequence ends
up being a specialized form of the “Sometimes Objecting” sequence.
In order to accomplish this goal, our sequence needs to be able to see some indicator which
tells it that primary test stimulus is willing to let the phase end. Luckily, the UVM’s Phases
provide just such an indicator, in the “Ready To End” state.
Our background sequence is going to have two threads of execution. . . one which is responsible
for waiting to see the phase go to “Ready To End”, and another which is responsible for
generating the random background traffic.
1 class background_sequence
2 extends uvm_sequence#();
3
4 ‘uvm_object_utils(background_sequence)
5 ‘uvm_declare_p_sequencer(sequencer_type)
6
7 function new(string name = "unnamed-background_sequence");
8 super.new(name);
9 endfunction : new
10
11 // The body task is just going to fork off two
12 // children tasks.
13 virtual task body();
14 fork : body_fork
15 wait_for_rte();
16 generate_noise();
17 join : body_fork
18 endtask : body
When our first thread detects that the phase is “Ready To End”, it is going to raise the
objection to the phase ending. This will move the phase back to the “Executing” state, as
the sequences quiesces.
32
20 virtual task wait_for_rte();
21 starting_phase.wait_for_state(UVM_PHASE_READY_TO_END);
22 starting_phase.raise_objection(this,
23 "winding down the sequence");
24 endtask : wait_for_rte
The second thread is executing inside of a while loop. So long as no objections have been
raised, it’s business as usual. If the thread detects that the objection has been raised, then
it knows that the phase is ending, and breaks out of the loop.
And finally, just like the “Sometimes Objecting” sequence, we need to clean up in the event
that we are killed when our objection is raised.
Another common scenario with Run-Time phasing is to prolong a phase until the DUT
reaches a desired state. One of the most common examples of this type of state→phase
connection would be to delay a phase until the scoreboards report that the DUT is idle.
33
Our first instinct may be to simply pass a reference to the Run-Time Phase into our score-
boards, that way our scoreboards can object to the phase directly. Unfortunately, that means
that the analysis side of our testbench now has a reference to something which is an entirely
stimulus-centric concept, and that’s usually a bad idea.
For example: Let’s say that we write a scoreboard, and inside of that scoreboard we imple-
ment the main_phase task. So long as the DUT is not idle, our scoreboard is going to object
to the main_phase ending.
What happens if our test writer wants to:
∙ have the scoreboard object to a different phase?
∙ have the scoreboard object to multiple phases?
∙ prevent the scoreboard from objecting to any phases?
We could always write our scoreboard to be extremely configurable, such that it can have
multiple (or zero) phases which it will object to, but now we’re complicating our scoreboard
and adding features which may never even be used by our test writers.
The UVM provides a simple solution to this problem: Instead of making the analysis domain
aware of the Run-Time Phases, make your stimulus aware of the analysis domain.
We can write a simple sequence which can be started during any phase we want (and can be
started in multiple phases if desired), which will look at our scoreboard’s status and prolong
the phase if/when the scoreboard is not idle.
Here’s an example showing the body() and do_kill() methods of such a sequence:
34
17 virtual function void do_kill();
18 // Drop the objection if we raised it
19 uvm_objection spo = starting_phase.get_objection();
20 spo.drop_objection(this,
21 "sequence has been killed",
22 spo.get_objection_count(this));
23 endfunction : do_kill
A few quick notes on that example: First, we’re not showing exactly how the wait_for()
method on lines 7 and 11 is implemented. This is mainly just to save space, but suffice it to say
we’ve established a connection (perhaps TLM or resources-based) which allows the sequence
to see the state of the scoreboard. For additional information, please refer to Proposal 6.10.
Second, we’re throwing a fatal error if our starting_phase hasn’t been assigned, because
there’s no sense in running this sequence unless you pass it a starting_phase.
Now that we have our sequence written, our test writer can choose whether or not they
want to prolong a phase, and the scoreboard writer doesn’t have to know anything about the
Run-Time Phasing schedule.
Yet another common interaction with the Run-Time phases is to enforce a minimum or
maximum execution time (or both). The library currently doesn’t support an explicit API
for enforcing these types of execution times, however it’s quite easy to write some simple
sequences to do the job for us.
A sequence to enforce a minimum phase execution time is basically just an “always objecting”
sequence, which uses the body to perform the actual delay.
35
Here’s an example of such a sequence:
Example Code 24: A sequence which delays a phase for some time
The example code above shows a very simple delay sequence, which always delays for 5ns,
however it would be reasonably easy to write a much more programmable/reusable version.
While the minimum execution time can be enforced by a simple “Always Objecting” se-
quence, the maximum execution time can be enforced via a standard (ie. “Never Objecting”)
sequence.
Here’s the example code:
Once again, the example code is showing a very simple version of a timeout sequence. So
long as the phase ends before the 10ns delay, the sequence will be terminated. . . otherwise
the timeout will expire and the sequence will terminate the simulation.
36
4.6 Connecting Stimulus To Our Schedules
Now that we’ve created our Schedules and our Phase Aware Stimulus, it makes sense that
we’d want to connect them! In this section, we’ll cover the basics of how those connections
are handled.
The UVM Library provides a mechanism for starting a single “default” sequence per phase,
per sequencer, using resources/configuration.
The values passed to the configuration database are:
context: The sequencer on which to start the phase
inst_name: The phase name on which to start the sequence
field_name: "default_sequence"
The UVM Library currently supports two different parameterized types for this configuration.
If the user wants to use the factory to create the sequence automatically, then they can pass
in the object wrapper.
1 uvm_config_db#(uvm_object_wrapper)::set(path.to.sequencer,
2 "phase_a",
3 "default_sequence",
4 my_seq_type::get_type());
On the other hand, if our user wants to create the sequence locally (perhaps to assign some
key fields within it, or to randomize it), then they can pass in the sequence by reference:
1 my_seq_type seq;
2 ‘uvm_create_on(seq, path.to.sequencer)
3 seq.randomize();
4 uvm_config_db#(uvm_sequence_bsae)::set(path.to.sequencer,
5 "phase_a",
6 "default_sequence",
7 seq);
Please refer to Proposal 6.13 for a potential enhancement to the default_sequence config-
uration within the Library.
37
4.6.2 Starting More Than One Sequence
In the prior section, we detailed how to start a single “default” sequence per sequencer, per
phase... but what if we wanted to start more than one sequence on the sequencer?
The UVM Library’s implementation of default_sequence inside of the configuration database
doesn’t leave much room for allowing you to set more than one sequence per phase, and there’s
a very good reason for this: How is the database supposed to know the difference between the
user wanting to run two default sequences simultaneously, vs. the user wanting to override
the first default sequence with the second?
While we could think of any number of complicated schemes for allowing the user to start
many sequences simultaneously, the UVM Library solved that problem for us a long time
ago, in the form of a virtual sequence.
Here’s a quick example of the body of a virtual sequence which starts two children in parallel:
All we have to do is set the default_sequence to be our virtual sequence, and our virtual
sequence will ensure both of its children are executed in parallel. Please refer to Proposal
6.14 which discusses this in more detail.
An additional extremely important note is needed when connecting stimulus to our Run-Time
phasing schedule, and that has to do with “Domains.”
As we said back in section 2.1, “Domains” are used as a filtration mechanism for Run-Time
Phases. Components (including sequencers) are only aware of a single Run-Time Domain.
38
As such, sequencers can only launch “default” sequences on phases within the sequencer’s
domain. If we were to pass in a default_sequence to a sequencer, pointing to a phase which
was outside of our sequencer’s domain, then the setting is silently ignored by the library.
Please refer to Proposal 6.15 which discusses this restriction in more detail.
There is one final note which is very important for “default” sequences. If the sequence is still
running after the phase has transitioned to the ENDED state, then that sequence’s body()
task is going to be terminated via a process::kill(). This is not the same thing as calling
the sequence’s kill() method. The sequence will be left in a “zombie” state, which may
eventually result in an error from the sequencer.
This has been filed as a bug in the Library (Mantis 4430). Fortunately we can detect that
this scenario is going to occur, and gracefully handle the situation.
The method below provides a means for detecting if a phase is going to end before a sequence
has completed executing, and if so it will call kill() on the sequence.
39
19 begin
20 // Second waiter to see if sequence is ending
21 seq.wait_for_sequence_state(ENDED);
22 end
23 join_any : two_waits
24
25 // Then kill this fork
26 p.kill();
27 end
28 join_none : guard
29 endfunction : kill_sequence_on_phase_end
This method has been written such that it can be placed at a very high package scope,
allowing for a large amount of reuse within our testbenches. Until the bug in the Library is
fixed, all Phase-Aware sequences which could be run as “default” sequences should call this
method within their pre_start:
40
5 Advanced Topic: Jumping
In all of the sections up to this point, we have discussed the progression of the phase graph
as a constant uni-directional flow. Phase A occurs, then Phase B, then Phase C. . . but what
happens if we want to go back to a previous point in the graph, or if we want to skip forward
to a point in the graph without necessary running all the phases between the current phase
and the target? That’s where the topic of “Phase Jumping” comes in to play, and more so
than any other topic, we consider this to be an “advanced” use case.
Before we dig in to examples of how and when to jump, we first need to spell out the basic
rules for what a legal jump target is.
A legal jump target is defined as any phase, wherein the following is true: If the Phaser
were to kill all of the phases that are within the path of the target (either before or after),
and then start the target phase, the graph would be in a state that it would have naturally
achieved by itself.
Let’s look at a pretty basic graph, and use it as our example for jumps:
Phase Y
Phase Z
Within that graph, the Phasing state machine has the following legal states:
1. Phase A
2. Phase Z and Phase B
3. Phase Z and Phase Y and Phase C
4. Phase Z and Phase Y and Phase X
5. Phase D
At any given point during Run-Time, the schedule must be in one of those states. Starting
in Phase A, our next legal state is Phase Z and Phase B (Where both Z and B are running
simultaneously). If we try and jump from Phase A → Phase B, then Phase Z never starts. . . if
41
we try and jump from Phase A → Phase Z, then B never starts. We call these illegal never-
started phases “Orphans.”
Here’s the full list of possible jump targets for Phase A:
Going through every possible iteration within every possible phase will get tedious rather
quickly, but identifying orphans is pretty easy. Any time you reach a branch in the graph,
and the jump target is only reachable via one of the branches, then that jump target is not
valid. Also, note that a Phase is a legal jump target for itself as well... if we were to jump
from Phase A to Phase A, we’d essentially be re-starting Phase A.
It’s also worth noting that just because a jump target isn’t valid now, that doesn’t mean
that it will never be a valid jump target. Taking a look at our example, Phase C is not a
valid jump target for Phase A, because Phases Z and Y are orphaned. . . however, Phase C is
a valid jump target for Phase X, because no orphans would be created by the jump.
The overall complexity of determining whether or not a given phase is a valid jump target is
directly related to the number of parallel phases which are in the graph. In a graph which is
entirely serial in nature, all points within the graph are legal jump targets.
In the previous section we discussed all of the legal jump targets within a graph, but just
because we can jump to a given target, that doesn’t mean that it’s safe to do so. In this
section we’re going to provide some additional guidelines on when the user should consider
it safe to jump.
When we decide to jump forwards, we are essentially “skipping” phases within the test flow.
Since those phases are not going to be run, any setup, configuration, resource allocation, etc.
simply isn’t going to be executed.
For example: Let’s say that we have an object ‘foo’, which we’re going to create during the
configure_phase. Inside of the main_phase, we’re going to look at the field ‘foo.value’ to
42
determine what type of stimulus we’re going to execute.
If someone were to jump the phase graph from the reset_phase to the main_phase, and in
the process skip the configure_phase, then we would run into a Null-Object Access error,
because ‘foo’ was never created.
Because of this, we recommend that forward jumps should be restricted to targeting the
post-run phases in the “Common” Domain (extract_phase, check_phase, etc.).
Unlike the forward jumps in the graph, backwards jumps in the graph are less likely to have
the problem of potential Null-Object Accesses. Note that they’re not impossible, they’re just
much less likely to occur.
Here’s an example of how we could hit a Null-Object Access when jumping backwards: Let’s
say that we’re running our same test from the previous section, and ‘foo’ is set to null at the
end of our main_phase. Our test proceeds through reset, configure and main, and everything
is going just fine as we proceed into the shutdown phase. During the shutdown_phase, the
test jumps back to main_phase again. . . but now ’foo’ is null! While this is a bit of a
contrived example, you can see how artificially moving the phase graph can sometimes break
Schedule Aware stimulus which wasn’t written robustly.
In addition to the Null-Object Access danger, the UVM Library does not support recon-
structing a testbench. Since this type of reconstruction would break the test, we recommend
that backwards jumps should be restricted to targeting the Run-Time Phases only. The user
should avoid ever trying to jump backwards to any phase in the “Common” Domain.
In the previous sections, we described the rules and guidelines for legal jumps. In this section,
we’re going to describe how the UVM Library handles the jump, and what the effects on the
currently executing phases are.
In section 2.4 we described how phases have four basic states when executing (STARTED,
EXECUTING, READY TO END and ENDED), and the basic flow from one state to the next.
The Library treats a jump as an interrupt condition, and forces the phase to immediately
transition to the ENDED state. We refer to this action as a “hard” jump.
Since the phase is moved into the ENDED state, the Library is going to ignore all further
attempts to prolong the Phase’s execution. This means that the objection is essentially
ignored from this point forward.
Additionally, instead of transitioning to CLEANUP after the ENDED state, the phase will
transition to:
43
State Description
JUMPING The library treats the ’jumping’ state as an alternative to the ’cleanup’ state.
The same basic operations are performed (any phase tasks are killed, and the
objection is cleared).
After the ’jumping’ state, the Library will transition the phase into the DONE state. If we
happen to be jumping forwards, then all phases between the current phase and the jump
target are also transitioned into the DONE state.
Finally, when all of these actions are complete, the jump target is transitioned into the
SCHEDULED state.
While not strictly defined within the UVM, a “Soft” jump generally refers to any time a
participant declares that they want the phase graph to move back (or forwards) to a legal
state, but they’re willing to let stimulus absorb some time to clean up and prepare for the
thread termination.
Unfortunately, the UVM Library doesn’t currently provide any form of API to support such
a concept, and all jumps which are executed are “Hard” jumps (jumps that are going to
terminate the running threads, regardless of safety).
Now that we’ve described what all of our legal jump targets are, and what the library is
going to do when we jump, we need to nail down one final concept area: How to safely react
to a jump.
Unlike the natural progression of the phase graph, “jumping” has the ability to end a phase,
even when someone may be objecting to that phase ending. While this may seem innocuous
enough at first, there’s actually some very dangerous consequences.
Let’s say we have a resource handler, which works like a simple deli counter. Stimulus comes
in and requests a ticket number. When the ticket number is called, stimulus does whatever
it needs with the resource, and then releases the ticket. Our resource handler sees the release
of the ticket, and knows that it’s safe to call the next ticket number.
Now let’s say that someone jumped our phase after the ticket was assigned, but before we
released our ticket. Since the thread has been terminated, our resource handler has essentially
been deadlocked.
Sometimes we can write our code to protect us from such deadlocks. . . in the example above,
we could have implemented a do_kill method inside of our sequence which freed the ticket
automatically, and now we’d be safe. Unfortunately, sometimes we can’t write our code to
44
protect us. The best we can do in these cases is detect that we’ve been killed during a
non-recoverable time.
NOTE: While you might not think a non-recoverable error like this is common, one was
actually discovered within the basic handshake between the uvm_sequence and
uvm_driver in earlier versions of the UVM Library. Users were killing threads
that had started a sequence, without calling kill() on the sequence. When the
sequencer tried to unblock wait_for_grant, the entire agent would get deadlocked.
This bug was fixed in the UVM Library 1.1b (which now detects it and throws an
error), however it goes to show just how dangerous it can be to terminate a thread.
45
6 Proposals
The proposals listed in this section are to be considered “Suggestions” for future revisions
of the UVM. Many of these proposals are intended to simplify the interaction model for end
users, as well as increasing the overall efficiency of the UVM Library.
The Accellera VIP Working Group tracks bugs using a system called “MANTIS”. Each Mantis
item declared within this section can be viewed at:
http://www.eda.org/mantis/view.php?id=NNNN
. . . where NNNN is the mantis number associated with the item. Some proposals (primarily
those which call for a community contribution) do not have Mantis numbers associated with
them.
The uvm_objection class provides two separate mechanisms for delaying the evaluation of
UVM_ALL_DROPPED: The drain time, and the all_dropped callback.
These mechanisms can be used with the phase_done objection, but only if there are partici-
pants in the phase. If uvm_phase raised and dropped an objection in zero time, immediately
prior to calling phase_started, then the standard drain time logic and callbacks would
trigger, allowing users to potentially prolong the phase even if no one is objecting.
Mantis 4468
The only instances of uvm_phase which actually need an objection are the task-based phase
NODES... the mechanism is unnecessary in function-based NODES, as well as all IMP,
SCHEDULE, TERMINAL, and DOMAIN instances.
This proposal is to remove the phase_done instantiation from all phases which are not task-
based NODES, and have the objection-related methods within the uvm_phase which access
the objection return an error when used outside of these types.
Mantis 4433
46
In Run-Time phasing, it’s entirely possible that users won’t want components to even be
aware of the schedule, and it’s also possible that users won’t want specific callbacks for each
phase. By making the class non-virtual, we allow the users to instantiate phases with no
callbacks, without having to resort to silly code like the simple_task_phase.
Mantis 4436
Once a phase has been placed into a schedule, the only way to get a reference to that phase
is by using the find(...) methods. Unfortunately, this has two limitations:
1. It requires that you know either the name (or IMP type) of the phase in advance, so
that you can pass it to the find() method.
2. find will return after finding the first matching phase. . . if there are multiple phases
with the same name (or IMP type), then there is no way to locate them.
As such, it is proposed that we add simple introspection to the uvm_phase:
get_predecessor_nodes() and get_successor_nodes() - Valid on any phase within a
schedule (including schedule phases). This returns an array of the predecessor/successor
nodes.
Using these methods, a user can now traverse the entire schedule programmatically.
Mantis 4469
Any time users create their own domains, they’re going to want to synchronize them to the
run_phase within the common domain. It makes sense for the library to simply provide this
functionality in a one-liner within the uvm_domain class.
Something akin to:
uvm_domain my_domain = new(“my_domain”);
my_domain.sync_to_run();
. . . would work just fine.
Mantis 4470
The uvm_sequence_base class provides the starting_phase reference to the user, such that
they can object as necessary. The majority of the time, when a sequence is going to object
47
to a phase ending, it’s going to do so during pre_start, and it’s going to drop the objection
during post_start and do_kill.
A large amount of user code could be simplified if we simply provided a “Automatically
object to the phase ending” mechanism in uvm_sequence_base.
Ie.
uvm_sequence_base::set_auto_objection(bit enable=1)
Now, instead of writing all of the code above, the user simply needs to call this method
within their constructor, and the library will handle the automatic objection.
Mantis 4432
Mantis 4431
48
6.8 Inherit Starting Phase
The starting_phase variable contained within uvm_sequence is only set automatically by the
library if you’re using default_sequence. This means that for any user code which intends
to provide that phase to children sequences, the parent must do:
‘uvm_create(child)
child.starting_phase = this.starting_phase;
‘uvm_send(child)
... which can get a bit tedious. This proposal builds on the previous proposal to try and
simplify this code for users.
We propose that in addition to the hardening described in the previous section, if the starting
phase has not yet been set, then get_starting_phase should traverse the sequence hierarchy
in an attempt to find the phase in which the sequence was started.
This would introduce a backwards-compatibility concern though, as currently the starting
phase of a child sequence is not automatically assigned to its parent’s value. As such, if the
committee decided to allow this change then we may wish to promote the change using a
multi-step approach (as was done in the required-constructor fix).
Mantis 4471
One of the more common mechanisms for dropping all objections is to simply pass the
return of get_objection_count into the drop_objection method. Unfortunately, while
uvm_phase provides drop_objection, it doesn’t provide get_objection_count.
This method should be promoted up to the uvm_phase class.
Mantis 4434
Currently, the UVM User’s Guide doesn’t show any code patterns for connecting stimulus to
the analysis domain. Users are left coming up with ad-hoc mechanisms, many of which (like
using TLM Analysis Fifos) are expensive and potentially even dangerous.
While there may not be any “always right” examples of such a connection, the UVM User’s
Guide should contain some examples of such code.
Mantis 4438
49
6.11 Contribution: Reusable Delay and Timeout Sequences
The example delay and timeout sequences could very easily be extended to much more
programmable versions which would satisfy the needs of most users. This would be an
excellent area for a contribution to uvmworld.org.
There have been conversations about adding an API to uvm_phase which would allow the
user to set a minimum/maximum phase time. Having such an API, which supported both
delays in arbitrary time scales (ns,ps,etc) as well as clock-based delays would be a significant
effort.
On the other hand, if we leave min/max time APIs like that out of the uvm_phase, and
instead show the users how to write sequences which accomplish the same goal, then we’ve
now avoided the complications caused by time scales/precision/etc.
In addition to supporting default sequences by type, the default sequence logic should support
setting the default sequence by type name. This would allow default sequences for a given
phase to be declared via the command line.
Mantis 3741
Now our users don’t have to keep creating sequences which simply fork.
50
6.15 Remove Single-Domain Restriction
The fact that a component is bound to one domain, and therefore default_sequences can
only be started on phases within a sequencer’s domain, is an arbitrary restriction which
doesn’t actually make sense for the Run-Time Phasing implementation.
The default_sequence implementation should be implemented in a manor such that se-
quences can be started on any phase, within any domain, as the phasing context of the default
sequence isn’t the sequencer, but instead it is the one who calls uvm_config_db::set().
Mantis 4472
51