100% found this document useful (1 vote)
285 views

Fun With Uvm Sequences Coding and Debugging - VH v15 I12

The document discusses how to write and use sequences in UVM testbenches. It covers creating basic sequences that generate transactions, running sequences on sequencers, executing transactions using drivers, controlling other sequences, and writing virtual sequences that manage other sequences.

Uploaded by

vjemman
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
285 views

Fun With Uvm Sequences Coding and Debugging - VH v15 I12

The document discusses how to write and use sequences in UVM testbenches. It covers creating basic sequences that generate transactions, running sequences on sequencers, executing transactions using drivers, controlling other sequences, and writing virtual sequences that manage other sequences.

Uploaded by

vjemman
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 11

Fun with UVM Sequences —

Coding and Debugging


by Rich Edelman — Mentor, A Siemens Business

In a SystemVerilog UVM [2] testbench, most activity is from many different places, but normally a test
generated from writing sequences. This article will might construct sequences and then run them –
outline how to build and write basic sequences, and they embody the test. For example a test might be
then extend into more advanced usage. The reader pseudo-coded as:
will learn about sequences that generate sequence
items; sequences that cause other sequences to LOAD ALL MEMORY LOCATIONS
occur and sequences that manage sequences on READ ALL MEMORY LOCATIONS,
other sequencers. Sequences to generate out of CHECK THAT EXPECTED VALUES MATCH.
order transactions will be investigated. Self-checking
sequences will be written.
There might be a sequence to write all memory
INTRODUCTION locations from A to B. And another sequence to read
A UVM sequence is a collection of SystemVerilog all memory locations from A to B. Or something
code which runs to cause “things to happen”. There simpler: A WRITE_READ_SEQUENCE that first writes
are many things that can happen. A sequence most all the memory locations and then reads all the
normally creates a transaction, randomizes it and memory locations.
sends it to a sequencer, and then on to a driver. In the
driver, the generated transaction will normally cause The test below creates a sequence inside a fork/
some activity on the interface pins. For example a join_none. There will be four sequences running in
WRITE_READ_SEQUENCE could generate a random parallel. Each sequence has a LIMIT variable set and
WRITE transaction and send it to the sequencer and starts to run at the end of the fork/join_none. Once all
driver. The driver will interpret the WRITE transaction of the forks are done, the test completes.
payload and cause a write with the specified address
and data. class test extends uvm_test;
`uvm_component_utils(test)

my_sequence seq;
...
task run_phase(uvm_phase phase);
phase.raise_objection(this);
for (int i = 0; i < 4; i++) begin
fork
automatic int j = i;
seq = new($sformatf("seq%0d", j));
seq.LIMIT = 25 * (j+1);
seq.start(sqr);
join_none
Figure 1 end
wait fork;
phase.drop_objection(this);
endtask
endclass
CREATING A SEQUENCE
A UVM sequence is just a SystemVerilog object that
is constructed by calling new. It can be constructed

12
RUNNING A SEQUENCE `uvm_fatal(get_type_name(), "Randomize FAILED")
finish_item(t);
— CREATING AND SENDING end
A SEQUENCE ITEM endtask
The sequence below, ‘my_sequence’, is a simple endclass
sequence which creates transactions and sends
them to the sequencer and driver. In the code
below, the body () task is implemented. It is a simple
for-loop which iterates through the loop LIMIT EXECUTING A SEQUENCE ITEM
times. LIMIT is a variable in the sequence which can
be set from outside. — THE DRIVER
The driver code is relatively simple. It derives
Within the for-loop, a transaction object is from a uvm_driver and contains a run_phase. The
constructed by calling new () or using the factory. run_phase is a thread started automatically by
Then start_item is called to begin the interaction the UVM core. The run_phase is implemented as
with the sequencer. At this point the sequencer a forever begin-end loop. In the begin-end block
halts the execution of the sequence until the driver the driver calls seq_item_port.get_next_item (t).
is ready. Once the driver is ready, the sequencer This is a task which will cause execution in the
causes ‘start_item’ to return. Once start_item has sequencer – essentially asking the sequencer for
returned, then this sequence has been granted the next transaction that should be executed. It may
permission to use the driver. Start_item should be that no transaction is available, in which case
really be called “REQUEST_TO_SEND”. Now that this call will block. (There are other non-blocking
the sequence has permission to use the driver, calls that can be used, but are beyond the scope of
it randomizes the transaction, or sets the data this article, and not a recommended usage). When
values as needed. This is the so-called “LATE the sequencer has a transaction to execute, then
RANDOMIZATION” that is a desirable feature. The the get_next_item call will unblock and return the
transactions should be randomized as close to transaction handle in the task argument list (variable
executing as possible, that way they capture the ‘t’ in the example below). Now the driver can
most recent state information in any constraints. execute the transaction.

After the transaction has been randomized, and the For this example, execution is simple – it prints a
data values set, it is sent to the driver for processing message using the transactions’ convert2string ()
using ‘finish_item’. Finish_item should really be call, and waits for an amount of time controlled by
called “EXECUTE_ITEM”. At this time, the driver the transactions ‘duration’ class member variable.
gets the transaction handle and will execute it.
Once the driver calls ‘item_done ()’, then finish_item Once that ‘execution’ is complete, the seq_item_
will return and the transaction has been executed. port.item_done () call is made to signal back to
the sequencer and in turn the sequence that the
transaction has been executed.
class my_sequence extends uvm_sequence#(transaction);
`uvm_object_utils(my_sequence)
class driver extends uvm_driver#(transaction);
transaction t; `uvm_component_utils(driver)
int LIMIT;
... transaction t;
task body(); ...
for (int i = 0; i < LIMIT; i++) begin task run_phase(uvm_phase phase);
t = new(“t”); forever begin
start_item(t); seq_item_port.get_next_item(t);
t.data = i+1; `uvm_info(get_type_name(),
if (!t.randomize()) $sformatf("Got %s", t.convert2string()), UVM_MEDIUM)
#(t.duration); // Execute...

13
seq_item_port.item_done(); See the appendix for the complete code.
end
endtask First the handles are constructed and a run limit
endclass is set up.

ping_h = new("ping_h");
ping_h.LIMIT = 25;
pong_h = new("pong_h");
CONTROLLING OTHER SEQUENCES pong_h.LIMIT = 40;
Sequences can have handles to other sequences;
after all, a sequence is just a class object with data
members, and a “task body()”, which will run as a
thread.
Then the handles get their “partner” handle.
Virtual Sequences
The so-called “virtual sequence” – a sequence which
may not generate sequence items, but rather starts ping_h.pong_h = pong_h;
pong_h.ping_h = ping_h;
sequences on other sequencers. This is a convenient
way to create parallel operations from one control
point.

A virtual sequence simply has handles to other


sequences and sequencers. It starts them or
Finally the two sequences are started in parallel.
otherwise manages them. The virtual sequence
may have acquired the sequencer handles by being
assigned from above, or by using a configuration fork
database lookup, or other means. It may have ping_h.start(sqr);
constructed the sequence objects, or have acquired pong_h.start(sqr);
them by similar other means. The virtual sequence is join
like a puppet master, controlling other sequences.

A virtual sequence might look like:


WRITING A
sequenceA_t sequenceA;
sequenceB_t sequenceB;
SELF-CHECKING SEQUENCE
sequencerA sqrA; A self-checking sequence is a sequence which causes
sequencerB sqrB; some activity and then checks the results for proper
behavior. The simplest self-checking sequence issues
task body(); a WRITE at an address, then a READ from the same
sequenceA.start(sqrA); address. Now the data read is compared to the data
sequenceB.start(sqrB);
written. In some ways the sequence becomes the

GOLDEN model.

class write_read_sequence extends my_sequence;


`uvm_object_utils(write_read_sequence)
...
Related Sequences
In the code snippet below, there are two sequences, task body();
ping and pong. They each have a handle to each for (int i = 0; i < LIMIT; i++) begin
t = new($sformatf("t%0d", i));
other. They are designed to take turns. First one start_item(t);
sends five transactions, then the other, and so on.

14
if (!t.randomize()) int x;
`uvm_fatal(get_type_name(), "Randomize FAILED") int y;
t.rw = WRITE;
finish_item(t); video_transaction t;
end
task body();
for (int i = 0; i < LIMIT; i++) begin screendots = xpixels * ypixels;
t = new($sformatf("t%0d", i)); rate = 1_000_000_000 / (60 * screendots);
start_item(t); forever begin
if (!t.randomize()) addr = 0;
`uvm_fatal(get_type_name(), "Randomize FAILED") for (x = 0; x < xpixels; x++) begin
t.rw = READ; for (y = 0; y < ypixels; y++) begin
t.data = 0; t = new($sformatf("t%0d_%0d", x, y));
finish_item(t); start_item(t);
if (!t.randomize())
// Check `uvm_fatal(get_type_name(), "Randomize FAILED")
if (t.addr != t.data) begin t.rw = WRITE;
`uvm_info(get_type_name(), $sformatf("Mismatch. t.addr = addr++;
Wrote %0d, Read %0d", t.duration = rate;
t.addr, t.data), UVM_MEDIUM) finish_item(t);
`uvm_fatal(get_type_name(), "Compare FAILED") end
end end
end end
endtask endtask
endclass endclass

A more complete traffic generator would adjust


the arrival rate based on the current conditions –
WRITING A TRAFFIC as the other traffic goes up or down, the video
GENERATOR SEQUENCE traffic generation rate should be adjusted.
A video traffic generator can be written to generate
a stream of background traffic that mimics or WRITING SEQUENCES THAT
models the amount of data a video display might
require. There is a minimum bandwidth that is
ARE SYNCHRONIZED WITH
required for video. That calculation will change with EACH OTHER
the load on the interface or bus, but a simplistic Sequences will be used to synchronize other
calculation is good enough for this example. The sequences (so called virtual sequences). Often
video traffic will generate a “screen” of data 60 times two sequences need to have a relationship between
a second. Each screen will have 1920 by 1024 dots. them formalized. For example they cannot enter
Each dot is represented by a 32 bit word. Using their critical regions together – they must go
these numbers, the traffic generator must create single-file. Or they can only run after some
471MB per second. common critical region has passed.

The code below declares two sequences


class video extends my_sequence;
`uvm_object_utils(video) which are to be synchronized (synchro_A_h
and synchro_B_h). It also declares a synchronizer.
int xpixels = 1920; There is nothing special about these classes –
int ypixels = 1024; they have simply agreed to use some technique
int screendots; to be synchronized, as shown on the next page.
int rate;
bit [31:0] addr;

15
The simple synchronizer with two states —
synchro synchro_A_h;
synchro synchro_B_h; GO and STOP.
synchronizer s;
typedef enum bit { STOP, GO } synchro_t;
s = new();
synchro_A_h = new("synchroA"); class synchronizer;
synchro_B_h = new("synchroB"); synchro_t state;
endclass

The synchronized sequences get a handle


to the synchronizer and a starting address. The class that uses a synchronizer to NOT
execute until told to do so.
synchro_A_h.s = s;
synchro_A_h.start_addr = 2;
synchro_B_h.s = s; class synchro extends my_sequence;
synchro_B_h.start_addr = 2002; `uvm_object_utils(synchro)

bit [31:0] start_addr;


bit [31:0] addr;
synchronizer s;

The synchronizer control is rather simple. It just says synchro_transaction t;


“GO” for 20 ticks and “STOP” for 100 ticks.
task body();
forever begin
fork addr = start_addr;
forever begin // Is it safe?
#100; while (s.state == STOP) begin
s.state = GO; #10;
#20; `uvm_info(get_type_name(), "Waiting...", UVM_MEDIUM)
s.state = STOP; end
end t = new($sformatf("t%0d", addr));
join_none start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.rw = WRITE;
t.addr = addr++;
finish_item(t);
The synchronized sequences are started. They run
end
to completion and then simply get restarted. They endtask
run forever. endclass

fork
forever begin
synchro_A_h.start(sqr);
end In simulation, the sequence waits until the
forever begin
synchro_B_h.start(sqr);
synchronizer is in the GO state. Once in the GO state,
end then the synchronized code generates a transaction
join_none using new, and then calls start_item/finish_item
to execute it. After waiting for access to the driver
and then executing, the synchronized sequence
comes back to the top of the loop and checks the

16
Figure 2

synchronizer state. It will either GO again, or STOP/


`uvm_info(get_type_name(), $sformatf("Serviced %0d",
WAIT as shown in the example above. t.VALUE), UVM_MEDIUM)
end
IMPLEMENTING AN endtask
endclass
INTERRUPT SERVICE ROUTINE
WITH SEQUENCES class driver extends uvm_driver#(transaction);
Sequences will be used to provide an "interrupt `uvm_component_utils(driver)
service routine". Interrupt service routines “sleep”
transaction t;
until needed. This is a unique kind of sequence. In interrupt_transaction isr;
this example implementation it creates an “interrupt bit done;
service transaction” and does start_item and finish_ int value;
item. In this way, it can send that ISR transaction
bit [31:0] mem[1920*1024];
handle to the driver. The driver is then going to hold
that handle UNTIL there is an interrupt. task interrupt_service_routine(interrupt_transaction isr_h);
`uvm_info(get_type_name(), "Setting ISR", UVM_MEDIUM)
As part of the drivers’ job of handling the done = 0;
SystemVerilog Interface, it will handle the interrupts. isr_h.DONE = 0;
In this case, handling the interrupt means that wait(done == 1);
some data is put into the “held handle” and isr_h.VALUE = value;
isr_h.DONE = 1;
then the handle is marked done. Meanwhile, the
endtask
interrupt service sequence has been waiting for the
transaction to be marked DONE. In some parlance task run_phase(uvm_phase phase);
this is known as ITEM REALLY DONE. In the UVM, forever begin
there are other mechanisms for handling this kind of seq_item_port.get_next_item(t);
thing, but they are unreliable and more complicated
if ($cast(isr, t)) begin
than this solution. fork
interrupt_service_routine(isr);
class interrupt_transaction extends transaction; join_none
`uvm_object_utils(transaction) end
int VALUE; else begin
bit DONE; ...
endclass // REGULAR driver processing
...
class interrupt_sequence extends my_sequence; if (AN INTERRUPT OCCURS) begin
`uvm_object_utils(interrupt_sequence) done = 1;
value = mem[t.addr];
interrupt_transaction t; end
end
task body(); seq_item_port.item_done();
forever begin end
t = new("isr_transaction"); endtask
start_item(t); endclass
finish_item(t);
wait(t.DONE == 1);

17
SEQUENCES WITH The open_door is constructed and then started using
normal means. Then a test program can issue reads
“UTILITY LIBRARIES” and writes simply as in the RED lines below.
Sequence "utility libraries" will be created and used.
Utility libraries are simple bits of code that are useful
open_door open_door_h;
for the sequence writer – helper functions or other
abstractions of the verification process. open_door_h = new("open_door");
fork
The open_door sequence below does just as its open_door_h.start(sqr);
name implies. It opens the door to the sequencer and begin
driver. Outside calls can now be made at will using bit [31:0] rdata;
for (int i = 0; i < 100; i++) begin
the sequence object handle (seq.read () and seq.write
open_door_h.write(i, i+1);
() for example). open_door_h.read(i, rdata);
if ( rdata != i+1 ) begin
class open_door extends my_sequence; `uvm_info(get_type_name(), $sformatf("Error: Wrote
`uvm_object_utils(open_door) '%0d', Read '%0d'",
i+1, rdata), UVM_MEDIUM)
read_transaction r; //`uvm_fatal(get_type_name(), "MISMATCH");
write_transaction w; end
end
task read(input bit[31:0]addr, output bit[31:0]data); end
r = new("r"); join_none
start_item(r);
if (!r.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
r.rw = READ;
r.addr = addr;
finish_item(r); CALLING C CODE
data = r.data; FROM SEQUENCES
endtask Calling C code (using DPI-C) from sequences is easy,
but there are a few limitations. DPI import and export
task write(input bit[31:0]addr, input bit[31:0]data);
w = new("w"); statements cannot be placed inside a class – so they
start_item(w); must be outside the class in file, global or package
if (!w.randomize()) scope. As such, they have no design or class object
`uvm_fatal(get_type_name(), "Randomize FAILED") scope.
w.rw = WRITE;
w.addr = addr;
w.data = data; import "DPI-C" function void c_code_add(output int z,
finish_item(w); input int a, input int b);
endtask export "DPI-C" function sv_code;

task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
wait(0);
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM) A DPI-C export function or task is just a SystemVerilog
endtask
endclass function or task that has been “exported” using the
export command.

function void sv_code(int z);


$display("sv_code(z=%0d)", z);
endfunction

18
A DPI-C import function or task is a C function
with a return value. For a task, the return value is
task body();
an int (See the SystemVerilog LRM for details). For
forever begin
a function, the return value, is whatever the return `uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
value should be. for (int i = 0; i < 10; i++) begin
for (int j = 0; j < 10; j++) begin
The simple void function c_code_add () is defined c_code_add(z, i, j);
below. It has two inputs and “returns” a value in t = new($sformatf("t%0d", i));
the pointer *z. This C function calls the exported start_item(t);
if (!t.randomize())
SystemVerilog function ‘sv_code ()’. `uvm_fatal(get_type_name(), "Randomize FAILED")
t.duration = z;
t.c: t.rw = WRITE;
#include "stdio.h" finish_item(t);
#include "dpiheader.h" end
end
void `uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
c_code_add(int *z, int a, int b) end
{ endtask
*z = a + b; endclass
sv_code(*z);
}

CALLING SEQUENCES
The dpiheader.h is a handy way to check the API FROM C CODE
for DPI-C. In this example, the dpiheader.h (below) Calling sequences from C code is harder than
is very simple. calling C code from sequences. This is because
sequences are class objects. Class objects do
not have “DPI-C Scope”, so in order to call into a
void c_code_add( int* z, int a, int b);
void sv_code( int z); sequence (or start a sequence), other means must
be used. There are many other references on
techniques to do this.

This sequence does nothing particularly special, it


generates transactions, but it does call a C function.
(c_code_add RED line below). In terms of writing
sequences that call C code, there is really nothing
special to do in terms of sequences. The DPI-C code
must be properly written and must be declared in a
proper scope, as shown below and above, right.

class use_c_code_sequence extends my_sequence;


`uvm_object_utils(use_c_code_sequence)

int z;

c_code_transaction t;
Figure 3

19
CONCLUSION
The reader of this article
now knows that sequences
are not mysterious or
things to be afraid of,
but rather that sequences
are simply "code" – usually
stimulus or test code.
That code can be written
Figure 4
to do many different things,
from the original “random
transaction generation”
to synchronization to interrupt service
routines. Sequences are just code –
important code that causes stimulus
generation and results checking.

REFERENCES
[1] SystemVerilog, 1800-2017 - IEEE
Standard for SystemVerilog--Unified
Hardware Design, Specification,
and Verification Language https://
ieeexplore.ieee.org/document/8299595/
citations#citations

[2] UVM LRM, https://standards.ieee.org/


Figure 5
standard/1800_2-2017.html

SEQUENCES AND All source code is available from the author.


TRANSACTIONS RECORDING Contact [email protected] for access.
In the example code discussed throughout this
article, each of the sequences is running in parallel This article was previously presented
– at the same time on the single sequencer. The at DVCon US 2019.
sequences (and recorded streams are listed below
as children of that sequencer).

Each row is a sequence executing. It is easy to see


in the two screenshots above (figures 4 and 5) how
the sequences each take turns sending and executing
a transaction on the driver.

20
VERIFICATION
ACADEMY
The Most Comprehensive Resource for Verification Training

33 Video Courses Available Covering


• DO-254
• UVM Framework
• UVM Debug
• Portable Stimulus Basics
• SystemVerilog OOP
• Formal Verification
• Metrics in SoC Verification
• Verification Planning
• Introductory, Basic, and Advanced UVM
• Assertion-Based Verification
• FPGA Verification
• Testbench Acceleration
• Power Aware Verification
• Analog Mixed-Signal Verification
UVM and Coverage Online Methodology Cookbooks
Discussion Forum with more than 10,000 questions asked
Verification Patterns Library

www.verificationacademy.com
Editor:
Tom Fitzpatrick

Program Manager:
Rebecca Granquist

Mentor, A Siemens Business


Worldwide Headquarters
8005 SW Boeckman Rd.
Wilsonville, OR 97070-7777

Phone: 503-685-7000

To subscribe visit:
www.mentor.com/horizons

To view our blog visit:


VERIFICATIONHORIZONSBLOG.COM

Verification Horizons is a publication


of Mentor, A Siemens Business
©2019, All rights reserved.

You might also like