Fun With Uvm Sequences Coding and Debugging - VH v15 I12
Fun With Uvm Sequences Coding and Debugging - VH v15 I12
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.
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
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
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
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.
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.
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
20
VERIFICATION
ACADEMY
The Most Comprehensive Resource for Verification Training
www.verificationacademy.com
Editor:
Tom Fitzpatrick
Program Manager:
Rebecca Granquist
Phone: 503-685-7000
To subscribe visit:
www.mentor.com/horizons