Mentor Technical Paper Uvm and C Tests Perfect Together
Mentor Technical Paper Uvm and C Tests Perfect Together
W H I T E P A P E R
F U N C T I O N A L V E R I F I C A T I O N
w w w . m e n t o r . c o m
UVM and C – Perfect Together
I. INTRODUCTION
This paper will demonstrate techniques and methods for using DPI-C along with a standard UVM Testbench. The C
code will take the form of low level transaction generator, high level transaction generator, scoreboard and monitor.
The UVM Testbench will be operating at the same time – for example the UVM tests may be streaming background
traffic on the bus, while the C code is creating specific bus transactions that are under test.
Agent
seque
sequence Monitor DUT
Sequencer Driver
1
UVM and C – Perfect Together
Agent
sence
sequence Monitor DUT
Sequencer Driver
C Code
Many agents (Figure 3) can be connected to the C code – either threaded or not. The C code is not instance specific.
There may be certain C code that is associated with certain interfaces (AHB vs AXI for example).
2
UVM and C – Perfect Together
uvm_sequencer_base sqr;
The interface above is a simple interface where an import and export are contained. It could contain many other
items. The helper functions act to simply forward a call into the interface to the connected sequencer. Only simple
wrappers are recommended in the interface. Keep the functionality in the sequencer or agent code.
driver d;
sequencer sqr;
3
UVM and C – Perfect Together
The agent has the usual functionality, and in the build_phase, we add two additional lines.
vif.sqr = sqr;
sqr.vif = vif;
These two lines connect the virtual interface and the sequencer. This is the only addition or change from a “regular”
agent description to a DPI-C enabled agent.
First, a handle to the virtual interface is added to the sequencer. Then any helper functions are created. The helper
functions could be located elsewhere, but this is a centralized place, and convenient.
...
task c_start_threads();
fork
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
join
endtask
There are two kinds of helper functions. The helper function that calls C code (c_start_threads()) and the helper
function that calls SV code (sv_start_sequenceC()).
The C example code uses the virtual interface handle to call the hosted C code. The scope is automatically set and
managed. In the example here, 4 threads are started at once.
4
UVM and C – Perfect Together
The SV example code creates a sequence and then starts it. Before starting it, any member variables or randomize
calls can happen. After the sequence completes, any results can be copied out (seq.b). The idea that a C function can
call into a UVM sequencer is very powerful. The C function call gets mapped by the helper functions into single or
multiple sequence executions. This is a key part of this solution. Create sequences that perform useful functions and
then call them from C code with helpers, using the interface as scope and the sequencer as a class based home for the
helpers.
int a;
int b;
transaction t;
virtual my_interface vif;
task body();
int i[10];
int o[10];
int io[10];
sequencer sqr;
$cast(sqr, m_sequencer);
vif = sqr.vif;
vif.c_hello(get_inst_id());
In this solution a sequence can retrieve the virtual interface handle from the sequencer it is running on.
sequencer sqr;
$cast(sqr, m_sequencer);
vif = sqr.vif;
vif.c_hello(get_inst_id());
In the case of the call to c_datatype_array_of_10_int, the example C code is called directly from the UVM sequence,
using the vif.
5
UVM and C – Perfect Together
void
c_datatype_array_of_10_int(const int* i, int* o, int* io)
{
int j;
IV. C CODE
void
c_hello(int inst_id)
{
const char *scopeName;
scopeName = svGetNameFromScope(svGetScope());
printf(" c: Hello! from %s. inst_id=%0d\n", scopeName, inst_id);
sv_hello(inst_id);
}
The simple hello world, is really more than a simple hello world. It is the way that C code is written and can be
made to call SystemVerilog using DPI-C. In the SV DPI-C specification, there are a few API calls, like svGetScope()
and svGetNameFromScope() that are sometimes useful. Normally you should not need any API calls. That’s one of
the things that makes DPI easy and powerful. You’re just using C code. That’s it.
When the SystemVerilog code wants to call C, then an ‘import’ call is used. When C code wants to call
SystemVerilog, then an ‘export’ call is used.
In the code above, the c_hello() is an import, and sv_hello() is an export. In the C code, to call our SystemVerilog
function or task, we simply call it.
C code could be written to perform any useful verification function – like reading a file of golden results, or
generating stimulus or collecting statistics. Using our solution, additionally the C code has the power to call sequences.
Any sequence that is available and has a helper function defined.
Threaded Code
C code is not normally thread-safe. In SystemVerilog, it is quite easy to create threads and threaded applications.
But these threaded applications must be “co-operative”. When a SystemVerilog “thread” starts, it has control of the
single compute. The only way for a different thread to gain control is for the current thread to give up control or yield.
Yielding can take many forms. If a SystemVerilog thread executes a #delay, or a wait() or a @(posedge clk), then
that thread will yield and the next thread will be able to gain control and run.
In the sequencer code above, there is a helper function which starts C threads
task c_start_threads();
fork
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
join
endtask
6
UVM and C – Perfect Together
When this code executes, 4 threads get created. Each of them runs in turn. (Until the running one yields. Only one
can run at a time).
In the example code, the task c_thread() calls the helper function sv_start_sequenceC(). This will create a sequence
and run it. In that sequence, transactions will be created and randomized, and then start_item() and finish_item() will
send them to the driver and on to the interface pins and the device-under-test.
But the C code doesn’t need to worry about those details. The C code is a piece of code which calls built in helper
functions. The C code and the helper functions must be written in a thread-safe way. For this example, we used a
global variable named ‘jj’, which is not thread-safe – its value will change from the time a thread goes to sleep, to the
time the thread wakes up. While a thread sleeps, if the state changes for that thread, then the code is not thread-safe.
j = 1;
jj = 1;
a = id;
sv_start_sequenceC(a, &b);
printf("a=%0d, b=%0d\n", a, b);
if (j != 1) printf("Error: 1 mismatch j\n"); j = 2;
if (jj != 1) printf("Error: 1 mismatch jj\n"); jj = 2;
sv_start_sequenceC(a, &b);
printf("a=%0d, b=%0d\n", a, b);
if (j != 2) printf("Error: 2 mismatch j\n"); j = 3;
if (jj != 2) printf("Error: 2 mismatch jj\n"); jj = 3;
sv_start_sequenceC(a, &b);
printf("a=%0d, b=%0d\n", a, b);
if (j != 3) printf("Error: 3 mismatch j\n"); j = 0;
if (jj != 3) printf("Error: 3 mismatch jj\n"); jj = 0;
return 0;
}
7
UVM and C – Perfect Together
Output Output
# Ping # Ping
# Pong # Ping
# Ping # Ping
# Pong # Ping
# Ping # Pong
# Pong # Pong
# Ping # Pong
# Pong # Pong
Output Output
# Ping # Ping
# Pong # Ping
# Ping # Ping
# Pong # Ping
# Ping # Pong
# Pong # Pong
# Ping # Pong
# Pong # Pong
8
UVM and C – Perfect Together
In Figure 4 the example code is running, and for each agent there is a line of colored class handles. The colors
represent the type of sequence that is running at that moment on that agent. Pink for C generated sequences and Blue
and Purple for SystemVerilog generated sequences. They all take turns – all background, or “normal” traffic is
sequenced on the interface, as well as the new C based sequences.
This ability to seamlessly integrate the UVM background traffic and the C generated traffic key a key point in this
solution.
V. THE C CODE
The goal of the C code is to interact with the device under test in some way. Either by generating input or monitoring
output or some of both. Interacting with the DUT is under control of the UVM, so any interaction from C must follow
the rules. Using the UVM sequencer and sequences is an easy an efficient way to do this.
All operations that might be imagined in C need to be broken down into sub-processing with each sub-process
represented by a UVM sequence. For example, a large data transfer from C to the DUT would be implemented as a
call to a “large-transfer” sequence, or many calls to a byte-wise transfer sequence.
C Bus transfers are tests which cause a transfer or transaction on a bus. For example, a READ or WRITE. Each call
to or from C is a bus transfer. The C call in turn creates a bus transfer sequence and executes it.
There are many ways C code can be used, all of which are beyond the scope of this paper to describe in detail.
Some examples include:
Stimulus Generator
Simply a stimulus generator. Data will be created on the C side and sent to the SV side with a DPI-C call.
9
UVM and C – Perfect Together
Data checker
A golden model written in C. Data from the DUT will be monitored and moved to the C side by calling an import
task or function.
Bus transfer generator
A program on the C side which issues bus READs and WRITEs. The C program has little knowledge that it is
running in a SystemVerilog simulation.
VI. CONCLUSION
Using DPI-C is easy and powerful. Using a SystemVerilog interface, many of the integration and connection issues
can be eliminated. Using the techniques outlined above large, threaded C tests can be created easily. Please contact
the author for downloadable source code to get started and experiment with DPI-C.
VII. REFERENCES
[1] SystemVerilog Language Reference Manual, http://standards.ieee.org/getieee/1800/download/1800-2012.pdf
[2] UVM Language Reference Manual, http://www.accellera.org/images/downloads/standards/uvm/uvm_users_guide_1.2.pdf
[3] “DPI Redux. Functionality. Speed. Optimization.”, DVCON 2017, Rich Edelman, Rohit Jain, Hui Yin.
VIII. APPENDIX
The C Code
#include <stdio.h> return 0;
#include "dpiheader.h" }
void /* --------------------------------- */
c_hello(int inst_id)
{ void
const char *scopeName; c_datatype_2d_array_of_int(
scopeName = svGetNameFromScope(svGetScope()); const int* i,
printf(" c: Hello! from %s. inst_id=%0d\n", int* o,
scopeName, inst_id); int* io)
sv_hello(inst_id); {
} }
10
UVM and C – Perfect Together
c_datatype_bit2( {
const svBitVecVal* i, }
svBitVecVal* o,
svBitVecVal* io) void
{ c_datatype_openarray_of_int(
} const svOpenArrayHandle i,
const svOpenArrayHandle o,
void const svOpenArrayHandle io)
c_datatype_bit33( {
const svBitVecVal* i, }
svBitVecVal* o,
svBitVecVal* io) void
{ c_datatype_real(
} double i,
double* o,
void double* io)
c_datatype_enum( {
const svLogicVecVal* i, }
svLogicVecVal* o,
svLogicVecVal* io) void
{ c_datatype_shortreal(
} float i,
float* o,
void float* io)
c_datatype_logic( {
svLogic i, }
svLogic* o,
svLogic* io) void
{ c_datatype_struct(
} const struct_t* i,
struct_t* o,
void struct_t* io)
c_datatype_logic2( {
const svLogicVecVal* i, }
svLogicVecVal* o,
svLogicVecVal* io) void
{ c_datatype_struct_packed(
} const svLogicVecVal* i,
svLogicVecVal* o,
void svLogicVecVal* io)
c_datatype_logic33( {
const svLogicVecVal* i, }
svLogicVecVal* o,
svLogicVecVal* io)
typedef struct {
int x;
byte y;
} simple_struct_t;
typedef struct {
int a;
bit b;
simple_struct_t simple_struct;
simple_struct_t simple_struct10[10];
bit [10:0] eleven_bits;
logic [10:0] eleven_logics;
bit [10:0] eleven_bits3[3];
logic [10:0] eleven_logics4[4];
} struct_t;
11
UVM and C – Perfect Together
import "DPI-C" context function void c_datatype_enum (input op_t i, output op_t o,
inout op_t io);
import "DPI-C" context function void c_datatype_bit (input bit i, output bit o,
inout bit io);
import "DPI-C" context function void c_datatype_logic (input logic i, output logic o,
inout logic io);
import "DPI-C" context function void c_datatype_bit2 (input bit [1:0] i, output bit [1:0] o,
inout bit [1:0] io);
import "DPI-C" context function void c_datatype_bit33 (input bit [32:0] i, output bit [32:0] o,
inout bit [32:0] io);
import "DPI-C" context function void c_datatype_logic2 (input logic [1:0] i, output logic [1:0] o,
inout logic [1:0] io);
import "DPI-C" context function void c_datatype_logic33 (input logic [32:0] i, output logic [32:0] o,
inout logic [32:0] io);
import "DPI-C" context function void c_datatype_struct (input struct_t i, output struct_t
o, inout struct_t io);
import "DPI-C" context function void c_datatype_struct_packed(input struct_packed_t i, output struct_packed_t
o, inout struct_packed_t io);
import "DPI-C" context function void c_datatype_real (input real i, output real o, inout
real io);
import "DPI-C" context function void c_datatype_shortreal (input shortreal i, output shortreal o, inout
shortreal io);
import "DPI-C" context function void c_datatype_array_of_10_int (input int i[10], output int o[10], inout
int io[10]);
import "DPI-C" context function void c_datatype_openarray_of_int(input int i[], output int o[], inout
int io[]);
import "DPI-C" context function void c_datatype_2d_array_of_int (input int i[10][5], output int o[10][5],
inout int io[10][5]);
//import "DPI-C" context function void c_datatype_queue_of_int(input int i[$], output int o[$], in out int
io[$]);
//import "DPI-C" context function void c_datatype_associative_array_of_int(input int i[int], output int
o[int], inout int io[int]);
uvm_sequencer_base sqr;
endinterface
`include "interface.svh"
12
UVM and C – Perfect Together
int a;
int b;
transaction t;
virtual my_interface vif;
task body();
sequencer sqr;
$cast(sqr, m_sequencer);
vif = sqr.vif;
vif.c_hello(get_inst_id());
int i[10];
int o[10];
int io[10];
int sqr_thread_id = 1;
13
UVM and C – Perfect Together
// ---------------
// Helper Routines
// ---------------
task c_start_threads();
fork
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
vif.c_thread(sqr_thread_id++);
join
endtask
transaction t;
driver d;
sequencer sqr;
agent a[10];
viflist_t viflist;
14
UVM and C – Perfect Together
sequenceA seqA[10];
sequenceB seqB[10];
e.viflist[0].c_hello(get_inst_id());
e.viflist[1].c_hello(get_inst_id());
wait fork;
phase.drop_objection(this);
endtask
endclass
module top();
reg clk;
my_interface interface0(clk);
my_interface interface1(clk);
my_interface interface2(clk);
my_interface interface3(clk);
my_interface interface4(clk);
my_interface interface5(clk);
my_interface interface6(clk);
my_interface interface7(clk);
my_interface interface8(clk);
my_interface interface9(clk);
viflist_t viflist;
initial begin
viflist[0] = interface0;
viflist[1] = interface1;
viflist[2] = interface2;
viflist[3] = interface3;
viflist[4] = interface4;
viflist[5] = interface5;
viflist[6] = interface6;
viflist[7] = interface7;
viflist[8] = interface8;
viflist[9] = interface9;
15
UVM and C – Perfect Together
run_test();
end
always begin
#10; clk = 0;
#10; clk = 1;
end
endmodule