Fun With Uvm Sequences Coding and Debugging
Fun With Uvm Sequences Coding and Debugging
Debugging
Rich Edelman
Mentor, A Siemens Business
46871 Bayside Parkway
Fremont, CA 94538
Abstract- In a SystemVerilogError! Reference source not found. UVM [2] test bench, most activity is generated from
writing sequences. This paper will outline how to build and write basic sequences, and then extend into more advanced
usage. The reader will learn about sequences that generate sequence items; sequences that cause other sequences to occur
and sequences that manage sequences on other sequencers. Sequences to generate out of order transactions will be
investigated. Self-checking sequences will be written.
I. INTRODUCTION
A UVM sequence is a collection of SystemVerilog code which runs to cause “things to happen”. There are many
things that can happen. A sequence most normally creates a transaction, randomizes it and sends it to a sequencer, and
then on to a driver. In the driver, the generated transaction will normally cause some activity on the interface pins. For
example a WRITE_READ_SEQUENCE could generate a random WRITE transaction and send it to the sequencer
and driver. The driver will interpret the WRITE transaction payload and cause a write with the specified address and
data.
S
WRITE_READ E
SEQUENCE Q
WRITE U
TRANSACTION E DRIVER DUT
N
C
E
R
II. CREATING A SEQUENCE
A UVM sequence is just a SystemVerilog object that is constructed by calling new. It can be constructed from many
different places, but normally a test might construct sequences and then run them – they embody the test. For example
a test might be pseudo-coded as
LOAD ALL MEMORY LOCATIONS
READ ALL MEMORY LOCATIONS, CHECK THAT EXPECTED VALUES MATCH.
There might be a sequence to write all memory locations from A to B. And another sequence to read all memory
locations from A to B. Or something simpler: A WRITE_READ_SEQUENCE that first writes all the memory
locations and then reads all the memory locations.
The test below creates a sequence inside a fork/join_none. There will be four sequences running in parallel. Each
sequence has a LIMIT variable set and starts to run at the end of the fork/join_none. Once all of the forks are done,
the test completes.
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
end
wait fork;
phase.drop_objection(this);
endtask
endclass
transaction t;
int LIMIT;
...
task body();
for (int i = 0; i < LIMIT; i++) begin
t = new(“t”);
start_item(t);
t.data = i+1;
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
finish_item(t);
end
endtask
endclass
transaction t;
...
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(t);
`uvm_info(get_type_name(), $sformatf("Got %s", t.convert2string()), UVM_MEDIUM)
#(t.duration); // Execute...
seq_item_port.item_done();
end
endtask
endclass
Virtual Sequences
The so-called “virtual sequence” – a sequence which may not generate sequence items, but rather starts 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 otherwise manages them.
The virtual sequence may have acquired the sequencer handles by being assigned from above, or by using a
configuration database lookup, or other means. It may have constructed the sequence objects, or have acquired them
by similar other means. The virtual sequence is like a puppet master, controlling other sequences.
A virtual sequence might look like
sequenceA_t sequenceA;
sequenceB_t sequenceB;
sequencerA sqrA;
sequencerB sqrB;
task body();
sequenceA.start(sqrA);
sequenceB.start(sqrB);
…
Related Sequences
In the code snippet below, there are two sequences, ping and pong. They each have a handle to each other. They are
designed to take turns. First one sends five transactions, then the other, and so on. See the appendix for the complete
code.
First the handles are constructed and a run limit is setup.
ping_h = new("ping_h");
ping_h.LIMIT = 25;
pong_h = new("pong_h");
pong_h.LIMIT = 40;
task body();
for (int i = 0; i < LIMIT; i++) begin
t = new($sformatf("t%0d", i));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.rw = WRITE;
finish_item(t);
end
// Check
if (t.addr != t.data) begin
`uvm_info(get_type_name(), $sformatf("Mismatch. Wrote %0d, Read %0d",
t.addr, t.data), UVM_MEDIUM)
`uvm_fatal(get_type_name(), "Compare FAILED")
end
end
endtask
endclass
video_transaction t;
task body();
screendots = xpixels * ypixels;
rate = 1_000_000_000 / (60 * screendots);
forever begin
addr = 0;
for (x = 0; x < xpixels; x++) begin
for (y = 0; y < ypixels; y++) begin
t = new($sformatf("t%0d_%0d", x, y));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.rw = WRITE;
t.addr = addr++;
t.duration = rate;
finish_item(t);
end
end
end
endtask
endclass
A more complete traffic generator would adjust the arrival rate based on the current conditions – as the other traffic
goes up or down, the video traffic generation rate should be adjusted.
s = new();
synchro_A_h = new("synchroA");
synchro_B_h = new("synchroB");
The synchronized sequences get a handle to the synchronizer and a starting address.
synchro_A_h.s = s;
synchro_A_h.start_addr = 2;
synchro_B_h.s = s;
synchro_B_h.start_addr = 2002;
The synchronizer control is rather simple. It just says “GO” for 20 ticks and “STOP” for 100 ticks.
fork
forever begin
#100;
s.state = GO;
#20;
s.state = STOP;
end
join_none
The synchronized sequences are started. They run to completion and then simply get restarted. They run forever.
fork
forever begin
synchro_A_h.start(sqr);
end
forever begin
synchro_B_h.start(sqr);
end
join_none
class synchronizer;
synchro_t state;
endclass
The class that uses a synchronizer to NOT execute until told to do so.
class synchro extends my_sequence;
`uvm_object_utils(synchro)
synchro_transaction t;
task body();
forever begin
addr = start_addr;
// Is it safe?
while (s.state == STOP) begin
#10;
`uvm_info(get_type_name(), "Waiting...", UVM_MEDIUM)
end
t = new($sformatf("t%0d", addr));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.rw = WRITE;
t.addr = addr++;
finish_item(t);
end
endtask
endclass
In simulation, the sequence waits until the synchronizer is in the GO state. Once in the GO state, then the
synchronized code generates a transaction 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 synchronizer state. It will either GO again, or STOP/WAIT in this example.
interrupt_transaction t;
task body();
forever begin
t = new("isr_transaction");
start_item(t);
finish_item(t);
wait(t.DONE == 1);
`uvm_info(get_type_name(), $sformatf("Serviced %0d", t.VALUE), UVM_MEDIUM)
end
endtask
endclass
transaction t;
interrupt_transaction isr;
bit done;
int value;
read_transaction r;
write_transaction w;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
wait(0);
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
The open_door is constructed and then started using normal means. Then a test program can issue reads and writes
simply as in the RED lines below.
open_door open_door_h;
open_door_h = new("open_door");
fork
open_door_h.start(sqr);
begin
bit [31:0] rdata;
for (int i = 0; i < 100; i++) begin
open_door_h.write(i, i+1);
open_door_h.read(i, rdata);
if ( rdata != i+1 ) begin
`uvm_info(get_type_name(), $sformatf("Error: Wrote '%0d', Read '%0d'",
i+1, rdata), UVM_MEDIUM)
//`uvm_fatal(get_type_name(), "MISMATCH");
end
end
end
join_none
import "DPI-C" function void c_code_add(output int z, input int a, input int b);
export "DPI-C" function sv_code;
A DPI-C export function or task is just a SystemVerilog function or task that has been “exported” using the export
command.
function void sv_code(int z);
$display("sv_code(z=%0d)", z);
endfunction
A DPI-C import function or task is a C function with a return value. For a task, the return value is an int (See the
SystemVerilog LRM for details). For a function, the return value, is whatever the return value should be.
The simple void function c_code_add () is defined below. It has two inputs and “returns” a value in the pointer *z.
This C function calls the exported SystemVerilog function ‘sv_code ()’.
t.c:
#include "stdio.h"
#include "dpiheader.h"
void
c_code_add(int *z, int a, int b)
{
*z = a + b;
sv_code(*z);
}
The dpiheader.h is a handy way to check the API for DPI-C. In this example, the dpiheader.h (below) is very simple.
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.
int z;
c_code_transaction t;
task body();
forever begin
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
for (int i = 0; i < 10; i++) begin
for (int j = 0; j < 10; j++) begin
c_code_add(z, i, j);
t = new($sformatf("t%0d", i));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.duration = z;
t.rw = WRITE;
finish_item(t);
end
end
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
end
endtask
endclass
Each row is a sequence executing. It is easy to see in the two screenshots below how the sequences each take turns
sending and executing a transaction on the driver.
XIV. CONCLUSION
The reader of this paper 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 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.
All source code is available from the author.
XV. 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/standard/1800_2-2017.html
import "DPI-C" context function void c_code_add(output int z, input int a, int b);
export "DPI-C" function sv_code;
constraint rw_value {
rw != IDLE;
}
constraint addr_value {
addr > 1;
addr < 10;
};
constraint value {
duration > 1;
duration < 10;
};
int VALUE;
bit DONE;
///////////////////////////////////////////////////////
transaction t;
int LIMIT;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
for (int i = 0; i < LIMIT; i++) begin
t = new($sformatf("t%0d", i));
start_item(t);
t.data = i+1 ; if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
finish_item(t);
end
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
int x;
int y;
video_transaction t;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
screendots = xpixels * ypixels;
rate = 1_000_000_000 / (60 * screendots);
$display("rate = %0d", rate);
forever begin
addr = 0;
screens++;
for (x = 0; x < xpixels; x++) begin
for (y = 0; y < ypixels; y++) begin
t = new($sformatf("t%0d_%0d", x, y));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.rw = WRITE;
t.addr = addr++;
t.duration = rate;
finish_item(t);
end
end
end
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
typedef enum bit { STOP, GO } synchro_t;
class synchronizer;
synchro_t state;
endclass
synchro_transaction t;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
forever begin
addr = start_addr;
while (s.state == STOP) begin
#10;
`uvm_info(get_type_name(), "Waiting...", UVM_MEDIUM)
end
t = new($sformatf("t%0d", addr));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.rw = WRITE;
t.addr = addr++;
finish_item(t);
end
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
interrupt_transaction t;
task body();
forever begin
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
t = new("isr_transaction");
start_item(t);
finish_item(t);
wait(t.DONE == 1);
`uvm_info(get_type_name(), $sformatf("Serviced %0d", t.VALUE), UVM_MEDIUM)
end
endtask
endclass
class open_door extends my_sequence;
`uvm_object_utils(open_door)
read_transaction r;
write_transaction w;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
wait(0);
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
read_transaction r;
write_transaction w;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
for (int i = 0; i < LIMIT; i++) begin
w = new($sformatf("t%0d", i));
start_item(w);
if (!w.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
w.rw = WRITE;
finish_item(w);
end
for (int i = 0; i < LIMIT; i++) begin
r = new($sformatf("t%0d", i));
start_item(r);
if (!r.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
r.rw = READ;
r.data = 0;
finish_item(r);
if (w.addr != r.data) begin
`uvm_info(get_type_name(), $sformatf("Mismatch. Wrote %0d, Read %0d",
w.addr, r.data), UVM_MEDIUM)
end
end
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
int z;
c_code_transaction t;
task body();
forever begin
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
for (int i = 0; i < 10; i++) begin
for (int j = 0; j < 10; j++) begin
c_code_add(z, i, j);
t = new($sformatf("t%0d", i));
start_item(t);
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
t.duration = z;
t.rw = WRITE;
finish_item(t);
end
end
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
end
endtask
endclass
pong pong_h;
ping_transaction t;
int LIMIT;
int waiting;
int done;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
waiting = 0;
for (int i = 0; i < LIMIT; i++) begin
if ((i % 5) == 0) begin
if (!pong_h.done) waiting = 1;
pong_h.waiting = 0;
end
wait(waiting == 0);
t = new($sformatf("t%0d", i));
start_item(t);
t.data = i+1 ;
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
`uvm_info(get_type_name(), $sformatf("Executing %s", t.convert2string()),
UVM_MEDIUM)
finish_item(t);
end
pong_h.waiting = 0;
done = 1;
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
ping ping_h;
pong_transaction t;
int LIMIT;
int waiting;
int done;
task body();
`uvm_info(get_type_name(), "Starting", UVM_MEDIUM)
for (int i = 0; i < LIMIT; i++) begin
if ((i % 5) == 0) begin
if (!ping_h.done) waiting = 1;
ping_h.waiting = 0;
end
wait(waiting == 0);
t = new($sformatf("t%0d", i));
start_item(t);
t.data = i+1 ;
if (!t.randomize())
`uvm_fatal(get_type_name(), "Randomize FAILED")
`uvm_info(get_type_name(), $sformatf("Executing %s", t.convert2string()),
UVM_MEDIUM)
finish_item(t);
end
ping_h.waiting = 0;
`uvm_info(get_type_name(), "Finished", UVM_MEDIUM)
endtask
endclass
transaction t;
interrupt_transaction isr;
bit done;
int value;
if (t.rw == WRITE)
mem[t.addr] = t.data;
else if (t.rw == READ)
t.data = mem[t.addr];
uvm_sequencer#(transaction) sqr;
driver d;
my_sequence seq;
ping ping_h;
pong pong_h;
open_door open_door_h;
synchro synchro_A_h;
synchro synchro_B_h;
synchronizer s;
video video_h;
interrupt_sequence isr_h;
write_read_sequence write_read_sequence_h;
use_c_code_sequence use_c_code_sequence_h;
fork
begin
#100_000;
phase.drop_objection(this);
end
join_none
fork
forever begin
video_h = new("video");
video_h.start(sqr);
end
join_none
fork
forever begin
isr_h = new("isr");
isr_h.start(sqr);
end
join_none
open_door_h = new("open_door");
fork
open_door_h.start(sqr);
begin
bit [31:0] rdata;
for (int i = 0; i < 100; i++) begin
open_door_h.write(i, i+1);
open_door_h.read(i, rdata);
if ( rdata != i+1 ) begin
`uvm_info(get_type_name(), $sformatf("Error: Wrote '%0d', Read '%0d'",
i+1, rdata), UVM_MEDIUM)
end
end
end
join_none
s = new();
synchro_A_h = new("synchroA");
synchro_B_h = new("synchroB");
synchro_A_h.s = s;
synchro_A_h.start_addr = 2;
synchro_B_h.s = s;
synchro_B_h.start_addr = 2002;
fork
forever begin
#100;
s.state = GO;
#20;
s.state = STOP;
end
join_none
fork
forever begin
synchro_A_h.start(sqr);
end
forever begin
synchro_B_h.start(sqr);
end
join_none
fork
forever begin
use_c_code_sequence_h = new("use_c_code_sequence_h");
use_c_code_sequence_h.start(sqr);
end
join_none
fork
forever begin
write_read_sequence_h = new("write_read_sequence_h");
write_read_sequence_h.LIMIT = 25;
write_read_sequence_h.start(sqr);
end
join_none
ping_h = new("ping_h");
ping_h.LIMIT = 25;
pong_h = new("pong_h");
pong_h.LIMIT = 40;
ping_h.pong_h = pong_h;
pong_h.ping_h = ping_h;
fork
forever begin
fork
ping_h.start(sqr);
pong_h.start(sqr);
join
end
join_none
fork
begin
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
end
wait fork;
end
join_none
module top();
initial
run_test();
endmodule
// =====================================================
//
// File: t.c
//
// =====================================================
#include "stdio.h"
#include "dpiheader.h"
void
c_code_add(int *z, int a, int b)
{
*z = a + b;
sv_code(*z);
}