Site hosted by Angelfire.com: Build your free website today!

This is an excerpt from the Chapter 1 of the book Principles of Verilog PLI by Swapnajit Mittra published from Kluwer Academic Publishers. ISBN: 0-7923-8477-6 [All copyright reserved. No portion of this text can be used for for commercial purpose without explicit permission from the author nor can it be used for re-print in hardcopy or electronic form.] Order the book here.

PLI - A QUICK TOUR

In this chapter we are going to take a quick tour to get ourselves acquainted with Programming Language Interface (PLI) of Verilog HDL.

We will go through the essentials of PLI without getting bogged down by too much of details. Our objective of this chapter would be to acquire a workable knowledge of PLI so that we can start writing basic programs. To achieve this, we will take an example of the very basic PLI activity - reading the value of a register from the design database. Once we have this we further modify it to do some other basic things using PLI. At the end, we will review what we have covered in this chapter and how it forms the basis for moving to the details of PLI.

WHAT IS A PLI ?

Programming Language Interface (PLI) is a way to provide Application Program Interface (API) to Verilog HDL. Essentially it is a mechanism to invoke a C function from a Verilog code. The construct which invokes a PLI routine in a Verilog code is usually called a system call. The common system calls built-in to most Verilog simulators are $display, $monitor, $finish etc.

WHY PLI IS REQUIRED

As the above examples of built-in PLI routines indicate, PLI is primarily used for doing such things which would not have been possible otherwise using Verilog syntax. For example, while Verilog has a way of doing file write ($fwrite, which is another built-in system call), it does not have any such construct for reading a register value directly from a file. There are other examples toowhere PLI is the only way of achieving the desired results. Some of them are:

  • Writing functional models
  • Delay calculations
  • Getting design information - for example, there is no Verilog construct which gives the name of the parent of the current module in thedesign hierarchy.
FOUR STEPS FOR WRITING A PLI ROUTINE

Traditionally, the main body of a PLI code is written in a file, which is called veriuser.c by convention. Though this name is completely user-defined, but while making a script for the compilation in Verilog XL, the vconfig tool will assume this name by default (We will discuss about this later in this chapter). For our discussion, we will assume that our PLI routine will be saved in the file veriuser.c.

To illustrate the basic steps for creating a PLI routine, the following problem is taken. This problem is much simplified compared to the actual 'real life' problems solved using PLI. However, this example shows all the basic steps that are required in all PLI routines.

Problem description
Create a system call $print_reg which takes a register as parameter and prints its value. (This is a simplified version of $display).

As an example, a verilog program like this:

module my_module;
reg [3:0] r1;
initial begin
r1 = 4'ha;
#100 $print_reg(r1);
#100 r1 = 4'h3;
#100 $print_reg(r1);
#100 $finish;
endmodule

Should produce the values 10 and 3 at times 100 and 300.

The following are the steps for writing a PLI routine.

Step 1: Including the header files

The file veriuser.c containing the main PLIC program must have

#include <veriuser.h>
#include <vxl_veriuser.h>

in Verilog XL and

#include <vcsuser.h>

in VCS.

As always in C, a header file within corner brackets (i.e. '<' and'>') means during the compilation the file should be either in the /usr/include directory or in a path include_path_name. In the later case, however, the program must be compiled with -Iinclude_path_name option. The include files can also be specified as shown below.

#include "include_path_name/veriuser.h"
#include "include_path_name/vxl_veriuser.h"

A typical example of include_path_name may look like /usr/caetools/verilog/.../include
The header file(s) contains the most basic data-structures of the Verilog-PLI that the program will use.

Step 2 : Function prototype and Variable declaration

This part of the program contains all local variables and the functions that it invokes as part of the system call. In the present case, as explained in the next step, it will be as shown below.

int my_calltf(), my_checktf();

However, if the functions are in separate files, they sould be declared as external functions.

extern int my_calltf(), my_checktf();

A typical PLI code, like any other C program, may need few other housekeeping variables.

Step 3: The essential data structure

There are a number of data structures that must be defined in a PLI program. These are the variables through which Verilog communicates with the C code. Although OVI (Open Verilog International - the organization responsible for standardizing Verilog) recommends only one mandatory datastructure (veriusertfs), the exact number and syntax of these data structures vary from simulator to simulator. Verilog-XL from Cadence Design Systems, for example, requires four such data structures and functions for any PLI routine to work, whereas VCS, from Chronologic needs none, but needs a separate input file. Going by the popularity ofVerilog XL, we are going to mention all four of them here; the users of Chronologic VCS and Veriwell may ignore the them. These data structures, in the order they should appear, are given below.
veriuser_version_str
This character pointer can be used to indicate a version of the compiled Verilog for your reference. It can be also set to NULL string.The string appears on standard out as well as goes to the Verilog log file when you run your compiled Verilog. A typical example of using it is shown below.

char *veriuser_version_str ="\n\
=============================\n\
VERILOG PLI FIRST_ATTEMPT \n\
($print_reg) \n\
=============================\n";

endofcompile_routines
This data structure, as the name suggests, can be used for declaring functions that will be invoked at the end of the simulation. The present example does not need it. Nonetheless there has to be a default definition as shown below.

int (*endofcompile_routines[])() ={0};

err_intercept
This function, which returns either true or false (i.e. a boolean type), can be used to enhance the error detection mechnism of the code and can be ignored for small applications. A typical example of this routine would be as shown below.

bool err_intercept(level, facility,code)
int level; char * facility; char *code;
{ return (true); }

veriusertfs
The main interaction between the C-code that one writes and the Verilog simulator is done through a table. The Verilog simulator looks at this table and figures out the properties that the system call corresponding to this PLI routine would be associated with. This array, called veriusertfs in Verilog XL, may not be called by any other name as the simulator will not recognize that. Each element of veriusertfs has a distinct function and is very important to achieve the overall objective of writing the PLI routine.

The number of rows in veriusertfs is same as that of the number of user-defined system calls, plus one for the last entry, which is a mandatory {0} .

In the present case, the veriusertfs array should look like:

s_tfcell veriusertfs[] =
{
/*** Template for an entry :
{usertask|userfunction,
data,
checktf(),
sizetf(),
calltf(),
misctf(),
"$tfname"
},
***/
{usertask, 0, my_checktf, 0, my_calltf,0, "$print_reg"},
{0} /* Final entry must be zero */
}

In the above data-structure my_checktf and my_calltf are the names ofthe two functions that we will use to implement the system call $print_reg. my_checktf is the routine which checks the validity of the passed parametersetc and my_calltf actually executes the system call. These names are arbitrary and could have been something else. The first entry usertask indicates that the system call does not return anything and is equivalent to procedure in Pascal or function returning void in C. Note that :

  • The relative positions of these names in veriusertfs are very important- for example my_checktf (or any other name ) has to be the 3rd element,as long as we want to use it as a checking function.
  • Any function that we do not use (e.g. misctf in our example. The objective of providing so many functions will be discussed in Chapter 2) should be replaced by a '0'.
  • We need to define a separate entry in veriusertfs for each system call in case there are more than one.
This table, in case of VCS, is written in a separate file, usually called PLI.tab (once again the name could be anything) whose content, in the present example, will be as shown below.

$print_reg check=my_checktf call=my_calltf


If you have liked this tutorial so far, guess
what's waiting for you in the book. Click here to order.

Step 4: tf routines

The checktf routine
Usually it is a good practice to check two things in a PLI routine

  • whether the total number of parameters are same as what is expected and
  • whether each parameter is of the required type.
For example, in our case, we expect only one parameter to be passedand it must be a register type. This is done in the following function my_checktf().

int my_checktf() {
if (tf_nump() != 1) {
tf_error("$print_reg:");
tf_error("Usage:$print_reg(register_name);");
}
if (tf_typep(1) != tf_readwrite) {
tf_error("$print_reg:");
tf_error ("The argument must be a register type\n");
}
}

The functions starting with tf_ are the library functions and commonly known as utility routines. (There is another set of library routines which start with acc_ and known as access routines. However, since in the present case only utility routines would be used, we will postpone the discussion of access routines to a later chapter) The library functions that we have used in the above function and their usages are given below.

  • tf_nump(): Determines how many parameters you are passing.
  • tf_typep(): Determines the type of the parameter by its position in the system call (in our case $print_reg) in the Verilog code. Thus tf_typep(1) gives the type of the first parameter, tf_typep(2) will get the type of the second parameter etc. In case the parameter does not exist(like tf_typep(2) in our case) tf_typep() returns error. Since we expect register to be passed, the type should be tf_readwrite. In case of a wire,the type should have been tf_readonly. Note that, to avoid error condition checking, it is a good idea to check the number of parameters first followed by their types.
  • tf_error(): Prints an error message and signals to the simulator to increment its error count.
These library functions and constants are defined in the header files that we included at the top.

The calltf routine
The calltf function is the heart of the PLI routine. Usuallyt he main job of the PLI routine is performed through this function. In our case it should read the value of the register and then print it.

int my_calltf(){
io_printf("$print_reg : The value of the specified register is %d", tf_getp(1));
}

The library functions used above and their usages are given below.

  • io_printf(): Does the same thing what printf() does in normal C programming- printing the value on standard output.
  • tf_getp(): Gives the integer value of the register.
In real life routines, however the calltf function is the most complicatedand often runs for several hundred lines.
For convenience, let us see the whole program at one place.

#include <veriuser.h>
#include <vxl_veriuser.h>
int my_calltf(), my_checktf();
char *veriuser_version_str ="\n\
=============================\n\
VERILOG PLI FIRST_ATTEMPT \n\
($print_reg) \n\
=============================\n";
int (*endofcompile_routines[])() ={0};
bool err_intercept(level, facility,code)
int level; char * facility; char *code;
{ return (true); }
s_tfcell veriusertfs[] =
{
/*** Template for an entry :
{usertask|userfunction,
data,
checktf(),
sizetf(),
calltf(),
misctf(),
"$tfname"
},
***/
{usertask, 0, my_checktf, 0,my_calltf, 0, "$print_reg"},
{0} /* Final entry must be zero*/
};

int my_checktf() {
if (tf_nump() != 1) {
tf_error ("$print_reg:");
tf_error ("Usage : $print_reg(register_name);\n");
}
if (tf_typep(1) != tf_readwrite){
tf_error ("$print_reg:");
tf_error ("The argument must be a register type\n");
}
}

void my_calltf(){
io_printf("$print_reg : The value of the specified register is %d", tf_getp(1));
}

COMPILING AND LINKING A PLI ROUTINE

When the Verilog simulator encounters a user defined system call, in order to make it understand the existance of the call and properly execute it, the PLI routine has to be compiled and then bind together with the existing binary of the simulator. Though the integration of the PLI code with the verilog binary can be done manually by running c-compiler and merging the object files, it is usually done through a script for convenience. In Verilog XL, a program called vconfig generates this scripts, whose name, by default, is cr_vlog. The program vconfig asks the name of the compiled verilog that you prefer. It also asks whether to include model libraries (which themselves are PLI code) from standard vendors. At the end it asks for the path for your veriuser.c file(s). Once the script cr_vlog is generated, just by running it one can get the new Verilog simulator binary which would recognize the user defined system call.

RESULTS OF RUNNING THE PLI ROUTINE

A sample run of the compiled Verilog having our PLI routine producesthe following output for Verilog-XL simulator.

=============================
VERILOG PLI FIRST_ATTEMPT
($print_reg)
=============================
Compiling source file "test.v"
Highest level modules:
my_module
$print_reg: The value of the specifiedregister is 15
$print_reg: The value of the specifiedregister is 3
L9 "test.v": $finish at simulationtime 400
15 simulation events
CPU time: 0.2 secs to compile + 0.1secs to link + 0.0 secs in simulation
End of VERILOG-XL 2.2.1 Jun 1, 199722:44:02

Self-test :
Repeat this experiment to a wire. The system call should readand print the value of the wire. Run the PLI routine on a net which istri1, rather than wire. Check its value when nobody is actively drivingit.

GETTING THE SIMULATION TIME

Although the PLI routine in the previous example worked as expected, from the result one can see there is one serious fault - from the result there is no way to figure out the simulation time when the register changed the value. With multiple invocation of this routine (as in this case) the problem becomes even worse.

This can be solved using another utility function tf_gettime()which tells the current time of the simulation. This has been done in a new revision of our calltf routine below by making minor modification tothe existing code. Also for convenience, the value of the register is displayed in hexadecimal rather than decimal.

void my_calltf(){
io_printf("$print_reg : The value of the specified register at time %d is %h", tf_gettime(), tf_getp(1));
}

As shown in the routine, tf_gettime() returns the current simulation time and it does not need any input parameter. A sample run of the modified version of our routine give the following output.

=============================
VERILOG PLI FIRST_ATTEMPT
($print_reg)
=============================
Compiling source file "test.v"
Highest level modules:
my_module
$print_reg: The value of the specified register at time 100 is a
$print_reg: The value of the specified register at time 300 is 3
L9 "test.v": $finish at simulationtime 400
15 simulation events
CPU time: 0.2 secs to compile + 0.1secs to link + 0.0 secs in simulation
End of VERILOG-XL 2.2.1 Jun 1, 199723:44:02

MODIFYING THE CONTENT OF A REGISTER

If reading the design information is one reason why PLI is used, the other reason would be to modify these informations in the design database. The following example shows how it is done.

Problem description
Create a system call $invert to print the value of a register (just like what was done in the last example), bit-wise invert the content andprint this updated value too. (So at the end the register will have one's complement of its current content.)

There are more than one ways of getting the inverted value. The algorithm described below may not be the smartest as far as doing one's complementis concerned, but it will give more insight into the PLI mechanism and library functions.

Read the content of the register is as a string;
Make the following modifications on the string:
Flip all 1s in the stringto 0s and all 0s to 1s;
All Zs become X and Xs,if any, remain intact.
Put back the modified string into the register.

The calltf function
The checktf function in this case will not be different from the earlier one as in both cases the number and type input parameter is same.The following code shows the invert_calltf - the calltf function for the $invert routine.

int invert_calltf() {
char *val_string;
/* The string which temporarily holdsthe value of the reg */
/* Step 1 of the algorithm */
val_string = (char *) malloc(tf_sizep(1)+1);
/* +1 to accomodate the null char at the end */
strcpy(val_string, tf_strgetp(1, 'b'));
/* Step 2 of the algorithm */
move('1', '2', val_string);
move('0', '1', val_string);
move('z', 'x', val_string);
move('2', '0', val_string);
/* Step 3 of the algorithm */
io_printf("$invert: Modifying the content from %s to %s at time %d\n",tf_strgetp(1, 'b'), val_string, tf_gettime());
/* The print statement should conatin the name of the PLI routine */
tf_strdelputp(1, tf_sizep(1), 'b', val_string, 0, 0);
}

The description and the usage of the library functions used above aregiven below.

  • tf_strgetp(): Reads the content of the register as a string, just like tf_getp() reads it as a decimal.
  • tf_strdelputp() : Assigns a value to the register as a string. This function takes a number of arguments. In order of their position they are : the parameter serial number, size of the string whose value is going to be put in (which, in the present case, should be same as the size of the register),the encoding radix, the actual string and two other delay related arguments,which we chose to ignore by passing 0s for them. (Though it will not be used in the present example, it is worth noting that, there is a simpler decimal counterpart of tf_strdelputp() too. It is called tf_putp()).
Here one important point to remember is that the content of only certain objects can be changed or overwritten from a PLI routine. Any attempt tochange a variable which can not be put on the left hand side of a procedural assignment in Verilog, would result in an error.

In general, the content of a variable, whose type is tf_readwrite, canbe modified. This is checked in the checktf function my_checktf().

Once tf_strgetp() reads the content in binary format ( 'h' or 'o' would have read it in hexadecimal or octal format ) it is copied to a string val_string.

move() is a function yet to be defined, which would replace each occurance of the first parameter (a character) by the second parameter (another character) in the third parameter (a string). Once 0s are converted to1s, to distinguish between these inverted 0s and original 1s in the string, at first all 1s are changed to 2 - an invalid value for binary logic, but a work around for us. At the end, all 2s
are converted back to 0s.

The program completes its task by putting back the inverted valueback into the register using tf_strdelputp() function.

Here is the code for move().

void move(from, to, in_str)
char from, to, *in_str;
{
int i=0;
while(*(in_str+i)) {
if (*(in_str+i) == from)
*(in_str+i) = to;
i++;
}
}

The complete code for the entire PLI routine is given below. Note thatthis time it is written for the VCS simulator.

#include "vcsuser.h"
/* In stead of the above, Verilog-XLuser should put :
#include "veriuser.h"
#include "vxl_veriuser.h"
*/
int invert_calltf(), my_checktf();
void move();
/* Verilog-XL users should have thefollowing :
char *veriuser_version_str ="\n\
=============================\n\
VERILOG PLI FIRST_ATTEMPT \n\
($invert)\n\
=============================\n";
int (*endofcompile_routines[])() ={0};
bool err_intercept(level, facility,code)
int level; char * facility; char *code;
{ return (true); }
s_tfcell veriusertfs[] =
{
{usertask,0,my_checktf,0,invert_calltf,0,"$invert"},
{0}
};
*/

int my_checktf() {
if (tf_nump() != 1) {
tf_error("$invert:\n");
tf_error("Usage: $invert(register_name);\n");
}
if (tf_typep(1) != tf_readwrite)
tf_error ("$invert: The argument must be a register type\n");
}

int invert_calltf() {
char *val_string;
/* The string which temporarily holdsthe value of the reg */

/* Step 1 of the algorithm */
val_string = (char *) malloc(tf_sizep(1)+1);
/* +1 to accomodate the null char at the end */
strcpy(val_string, tf_strgetp(1, 'b'));

/* Step 2 of the algorithm */
move('1', '2', val_string);
move('0', '1', val_string);
move('z', 'x', val_string);
move('2', '0', val_string);

/* Step 3 of the algorithm */
io_printf("$invert: Modifying the content from %s to %s at time %d\n",tf_strgetp(1, 'b'), val_string, tf_gettime());
/* A message should always conatin
the name of the PLI routine */
tf_strdelputp(1, tf_sizep(1), 'b',
val_string, 0, 0);
}

void move(from, to, in_str)
char from, to, *in_str;
{
int i=0;
while(*(in_str+i)) {
if (*(in_str+i) == from)
*(in_str+i) = to;
i++;
}
}

For VCS, the equivalent of veriusertfs[] structure is given in a file pli.tab whose content in this case is given below.

$invert call=invert_calltf check=my_checktf

RUNNING AGAIN

A sample Verilog program containing the call $invert is shown below.

module mymodule;
reg [7:0] r1;
initial begin
r1 = 8'hf;
#100 $invert(r1); // Invert the content of r1
#100 $finish;
end

Assuming the code is saved in the file test.v, it is compiled for VCS to generate the executable binary pli_invert by giving the followingcommand.

$ vcs -o pli_invert -P pli.tab test.vveriuser.c

Executing pli_invert, we get the following result.

Contains Chronologic Simulation proprietary information. Oct 21 00:05 1997

$invert: Modifying the content from00001111 to 11110000 at time 100
$finish at simulation time 200
V C S S im u l a t i o n R e p o r t
Time: 200
CPU Time: 0.067 seconds;Data structure size: 0.0Mb
Tue Oct 21 00:05:16 1997

Self-test :
Modify the PLI routine $invert() to create a new system call $twos_complement(), which takes a register as its argument and replacesits content by the two's complement of its present value. Remember, you should issue an error message and abort, if any bit of the register is Z or X.
Repeat the problem again, so that the modified value of the registerappears only 10 time units after the current simulation cycle.

SUMMARY

In this chapter, we have learnt the basic structure of a PLI routine and mechanism of linking it to build a custom version of Verilog. We have also learnt how to read, convert and modify the values of elementary components of a design database in Verilog through PLI C code. And also how to read the simulation time. These are the basic and often the most needed tasks of a PLI code.

Compared to the rest of the universe that PLI offers, however, this is really a small part. In later chapters we will see how PLI can be used to access other design information not related to the basic component --name of the current module and its parent, or controlling the path-delays, for example.

Also we have so far used only the so-called 'utility routines' providedby PLI1.0. There is a complete different set of routines, known as'access routines', which is often recommended for more uniform interfacing.We will learn about it in Chapter 4.


All product names mentioned here are are registered trademarks of their respective owner companies.