Functional Features

TF (task/function) subroutines are mainly used for data transfer in two directions at the boundary between Verilog and user C programs.

TF subroutines always have the prefix tf_ and are defined in the header file veriuser.h. Therefore, when writing system tasks or functions in C, you need to add #include "veriuser.h" to the C file.

TF subroutines can be divided into the following uses:

  • Getting system task information
  • Getting parameter list information
  • Getting parameter values
  • Passing parameter values back to system tasks
  • Monitoring changes in parameter values
  • Getting simulation time and scheduled event information
  • Arithmetic operations
  • Displaying information
  • Managing and maintaining tasks
  • Other tasks such as suspension, termination, saving, and restoration

For a complete list of TF subroutines and their simple usage instructions, refer to the next section."8.3 TF Subroutine List"。

TF Subroutine Examples

The PLI subroutine library has a large number of functions. Explaining them one by one would require a great deal of space. Therefore, it is recommended to study individual subroutines carefully only when needed. Below are examples of some TF subroutines, mainly illustrating the basic flow of designing Verilog system tasks using TF subroutines.

Design Requirements

Sometimes, to reduce the size of the object file after compiling the C language file, or to output debug information in a certain format, the printing function in the software may not use C's built-in printf function, nor the io_printf function from TF subroutines. Instead, it uses the PLI interface and the Verilog system display functions ($display or $write) to implement a user-defined software printing function.

This design implements a software printing function named print_my, with an output format of "string + integer".

Design Analysis

The user-defined software printing function print_my is just an unattractive shell used to pass printing information to software formal parameter variables. To pass the printing information into Verilog, a system task built with TF subroutines is also needed, named $send_my().

It should be noted that system tasks implemented with TF subroutines generally need to be called in Verilog code. The software directly calling this "system task" is equivalent to calling a software function, and has no direct connection with the descriptions of related variables in the Verilog code. Therefore, directly calling the "software function" send_my in the software function print_my is meaningless. It is necessary to add some specific software behaviors in print_my to trigger the system task $send_my to be called by Verilog.

If the digital system contains a CPU, you can use the method of writing to the bus or memory via software, and then monitoring the relevant signal variables in the testbench. This design is relatively small in scale, so another method can be used: Verilog keeps executing the system task $send_my, and a global variable is set in the software function print_my to control whether to perform information transfer and printing.

Software Design

The software design code is as follows, with details explained in the comments. Save it to the file print_gyc.c.

Example

#include "veriuser.h"

// string to print, integer data, software print start flag
char            *str_mes ;  
unsigned int    int_mes ;
int             flags = 0;

//===== define print_my() used in C ======
void print_my(char * str_send, unsigned int int_send){
    str_mes  = str_send ;
    int_mes  = int_send ;
    flags    = 1 ;
}


// character-type data of the ASCII codes corresponding to the string to print, original character length limited to 100
// for example, the character "1" corresponds to ASCII code 0x31, whose character-type data is 0x33, 0x31
char            str_mes_format[200] ;
// when TF subroutines pass string-type data, they can only pass data in the relevant base format
// for example, "0xc0de1234" can be passed, but "www.example.com" cannot
// therefore, the original string data needs to be converted into character-type data of the corresponding ASCII codes
void byte2hexstr(char* str, char* dest, int len)
{
    char tmp;
    int  i ;
    char stb[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
    for (i = 0; i < len; i++){
        tmp = str[i];
        dest[i * 2] = stb[tmp >> 4];
        dest[i * 2 + 1] = stb[tmp & 15];
    }
    return;
}

//===== define PLI: send_my() =======
void send_my(){
    int i = 0 ;
    // detect the software print start flag flags
    // and the system task $send_my is called by Verilog with 3 parameters passed in
    if (flags && tf_nump()==3) {
        // convert the string data to be printed into the string type corresponding to ASCII
        byte2hexstr(str_mes, str_mes_format, 100);
        // pass the hexadecimal character-type data to the 2nd parameter in the system task send_my
        // length is 1600 bits, corresponding to 100 chars and 200 ASCII character-type data
        tf_strdelputp(2, 1600, 'H', str_mes_format, 0, 0);
        // pass the integer data to the 3rd parameter
        tf_putp(3, int_mes);
        // pass the hardware print start flag to the 1st parameter
        tf_putp(1, 1) ;
        flags = 0 ;
    }
}

Hardware Design

According to simulation testing, the data passed from software to Verilog is stored in a 1600-bit wide register as shown in the figure below.

When the register contains normal data, it starts storing normal character data from the 799th bit (half the register width). The remaining low-order bits store "NULL" (corresponding to data 0) and system default data. The lengths of these two parts of data vary according to the length of the normal data stored.

When the register contains no normal data, it uses more than 800 bits of register length to store "NULL" (corresponding to data 0). The remaining width stores system default data.

Based on the above characteristics, normal string data can be detected without printing all the data in the register, avoiding a large amount of blank space in the printed output that would affect appearance.

The hardware Verilog code design is as follows, with specific details explained in the comments. Save it to the file test.v.

Example

`timescale 1ns/1ps
module test ;
   reg [1599:0]         str_my ;
   reg [31:0]           int_my ;
   reg                  flag_my ;

   // continuously check the hardware print flag flagh to determine whether to print
   integer              i = 1599;
   integer              j = 1599;
   reg [7:0]            str_tmp ; // string data storage register
   initial begin
      flagh = 0 ;
      forever begin
         #20 ;
         // call the system task and pass data from software to hardware (Verilog)
         $send_my(flagh, str_my, int_my);
         if (flagh) begin  // start printing information
            #1 ;
            // register width has redundancy; printing everything would produce a lot of spaces
            // here, unused register bits are detected and suppressed from printing
            for(i=1599; i>=0; i= i-8) begin
               if (str_my[i -: 8] != 0)
                 break ;
            end
            // generally, half the register width is used to store string data
            // so when string transfer is correct, data starts from str_my
            $display("--DEBUG--- Valid data number: %d", i);

            // if there is no string data, str_my will not be completely empty either
            // but str_my[799 -: 8] will not have data
            if (i<=791) begin
               $display("--ERR--- PLEASE INPUT VALID STRING INFO!!!");
               $display("--ERR--- Default data number: %d", i);
               $display("--ERR--- String data structure: %h", str_my);
            end
            else begin
               $write("---YYY---");
               for(j=i; j>=0; j= j-8) begin
                  str_tmp = str_my[j -: 8] ;
                  // stop printing when a null character is detected again
                  if (str_tmp == "") begin  
                     break ;
                  end
                  // print character by character
                  else begin
                     $write("%s", str_tmp);
                  end
               end
               // print integer data
               $write("%h \n", int_my);
            end

            /* $display does not support variable access in register vector part-selects
// so the following description is incorrect; you can only use $write repeatedly to print
            else begin
               $display("--YYY--- %s%h", str_my[i : j], int_my);
            end
             */


            flagh = 0 ;
         end
      end
   end

   // stop the simulation
   initial begin
      forever begin
         #10000;
         if ($time >= 300)  $finish ;
      end
   end
endmodule

Software Invocation

Since the software part of this design cannot execute actively, an additional Verilog system task designed with TF subroutines is added. When this system task is executed in the testbench, it can call the function print_my in the software program.

Add the following C code to the file print_gyc.c:

void call_c_print(){
    if (tf_getp(1) == 1)   // if the first parameter value of the system function is 1
        print_my("It's the first successfull print: ", 0x20170714);
    else if (tf_getp(1) == 2) // if the second parameter value of the system function is 2
        print_my("2nd: ", 0x09070602);
    else
        print_my("", 0x1); // if string data is not transferred, report an error
}

Add the following Verilog code to the file test.v:

   //c print
   initial begin
      #100 ;
      $call_c_print(1);
      #100 ;
      $call_c_print(2);
      #100 ;
      $call_c_print(3);
   end

Compilation and Simulation

Under Linux, use the following command to compile print_gyc.c and output the print_gyc.o file. Pay attention to relative paths.

gcc -I ${VCS_HOME}/include -c ../tb/print_gyc.c

When compiling with VCS, you need to create a link table file that VCS can recognize, named pli_gyc.tab, with the following content.

$send_my and $call_c_print are the names of the system tasks called by Verilog;

call=my_monitor, call=call_c_print, etc., indicate calls to functions in the software C program;

$send_my call=send_my
$call_c_print call=call_c_print

Add the following parameter line when compiling with VCS.

-P ../tb/pli_gyc.tab

The simulation results are as follows.

As can be seen from the figure, the printed output is normal with no extra blank spaces.

When there is no string data in the printed information, an Error is reported, along with some debug information output.

You can modify some parameters in the testbench to debug and learn about data storage formats.

Chapter Source Code Download

Download