Using C++

Encapsulate the DUT hardware runtime environment with C++ and compile it into a dynamic library.

Principle Introduction

Basic Library

In this chapter, we will introduce how to use Picker to compile RTL code into a C++ class and compile it into a dynamic library.

  1. First, the Picker tool parses the RTL code, creates a new module based on the specified Top Module, encapsulates the module’s input and output ports, and exports DPI/API to operate the input ports and read the output ports.

    The tool determines the module to be encapsulated by specifying the file and Module Name of the Top Module. At this point, you can understand Top as the main function in software programming.

  2. Next, the Picker tool uses the specified simulator to compile the RTL code and generate a DPI library file. This library file contains the logic required to simulate running the RTL code (i.e., the hardware simulator).

    For VCS, this library file is a .so (dynamic library) file, and for Verilator, it is a .a (static library) file. DPI stands for Direct Programming Interface,which can be understood as an API specification.

  3. Then, the Picker tool renders the base class defined in the source code according to the configuration parameters, generates a base class (wrapper) for interfacing with the simulator and hides simulator details, and links the base class with the DPI library file to generate a UT dynamic library file.

    • At this point, the UT library file uses the unified API provided by the Picker tool template. Compared with the simulator-specific API in the DPI library file, the UT library file provides a unified API interface for the hardware simulator generated by the simulator.
    • The generated UT library file is common across different languages! Unless otherwise specified, other high-level languages will operate the hardware simulator by calling the UT dynamic library.
  4. Finally, based on the configuration parameters and parsed RTL code, the Picker tool generates a C++ class source code. This source code is the definition (.hpp) and implementation (.cpp) of the RTL hardware module in the software. Instantiating this class is equivalent to creating a hardware module.

    This class inherits from the base class and implements the pure virtual functions in the base class to instantiate the hardware in software. There are two reasons for not encapsulating this class implementation into the dynamic library:

    1. Since the UT library file needs to be common across different languages, and different languages have different ways to implement classes, for universality, the class implementation is not encapsulated into the dynamic library.
    2. To facilitate debugging, enhance code readability, and make it easier for users to repackage and modify.

Generating Executable Files

In this chapter, we will introduce how to write test cases and generate executable files based on the basic library generated in the previous chapter (including dynamic libraries, class declarations, and definitions).

  1. First, users need to write test cases, which means instantiating the class generated in the previous chapter and calling the methods in the class to operate the hardware module. Details can be found in [Random Number Generator Verification - Configure Test Code](docs/quick-start/examples/rmg/#Configure Test Code) for instantiation and initialization process.

  2. Second, users need to apply different linking parameters to generate executable files based on the different simulators applied in the basic library. The corresponding parameters are defined in template/cpp/cmake/*.cmake.

  3. Finally, according to the configured linking parameters, the compiler will link the basic library and generate an executable file.

    Taking Adder Verification as an example, picker_out_adder/cpp/cmake/*.cmake is a copy of the template described in item 2 above. vcs.cmake defines the linking parameters of the basic library generated using the VCS simulator, and verilator.cmake defines the linking parameters of the basic library generated using the Verilator simulator.

Usage

  • The parameter --language cpp or -l cpp is used to specify the generation of the C++ basic library.
  • The parameter -e is used to generate an executable file containing an example project.
  • The parameter -v is used to retain intermediate files when generating the project.
#include "UT_Adder.hpp"

int64_t random_int64()
{
    static std::random_device rd;
    static std::mt19937_64 generator(rd());
    static std::uniform_int_distribution<int64_t> distribution(INT64_MIN,
                                                            INT64_MAX);
    return distribution(generator);
}

int main()
{
#if defined(USE_VCS)
    UTAdder *dut = new UTAdder("libDPIAdder.so");
#elif defined(USE_VERILATOR)
    UTAdder *dut = new UTAdder();
#endif
    // dut->initClock(dut->clock);
    dut->xclk.Step(1);
    printf("Initialized UTAdder\n");

    struct input_t {
        uint64_t a;
        uint64_t b;
        uint64_t cin;
    };

    struct output_t {
        uint64_t sum;
        uint64_t cout;
    };

    for (int c = 0; c < 114514; c++) {
        input_t i;
        output_t o_dut, o_ref;

        i.a   = random_int64();
        i.b   = random_int64();
        i.cin = random_int64() & 1;

        auto dut_cal = [&]() {
            dut->a   = i.a;
            dut->b   = i.b;
            dut->cin = i.cin;
            dut->xclk.Step(1);
            o_dut.sum  = (uint64_t)dut->sum;
            o_dut.cout = (uint64_t)dut->cout;
        };

        auto ref_cal = [&]() {
            uint64_t sum = i.a + i.b;
            bool carry   = sum < i.a;

            sum += i.cin;
            carry = carry || sum < i.cin;

            o_ref.sum  = sum;
            o_ref.cout = carry ;
        };

        dut_cal();
        ref_cal();
        printf("[cycle %llu] a=0x%lx, b=0x%lx, cin=0x%lx\n", dut->xclk.clk, i.a,
            i.b, i.cin);
        printf("DUT: sum=0x%lx, cout=0x%lx\n", o_dut.sum, o_dut.cout);
        printf("REF: sum=0x%lx, cout=0x%lx\n", o_ref.sum, o_ref.cout);
        Assert(o_dut.sum == o_ref.sum, "sum mismatch");
    }

    delete dut;
    printf("Test Passed, destory UTAdder\n");
    return 0;
}

Generating Waveforms

In C++, the destructor of the DUT automatically calls dut.finalize(), so you only need to delete dut after the test ends to perform post-processing (write waveform, coverage files, etc.).

#include "UT_Adder.hpp"

int main()
{
    UTAdder *dut = new UTAdder("libDPIAdder.so");
    printf("Initialized UTAdder\n");

    for (int c = 0; c < 114514; c++) {
    
        auto dut_cal = [&]() {
            dut->a   = c * 2;
            dut->b   = c / 2;
            dut->cin = i.cin;
            dut->xclk.Step(1);
            o_dut.sum  = (uint64_t)dut->sum;
            o_dut.cout = (uint64_t)dut->cout;
        };

        dut_cal();
        printf("[cycle %llu] a=0x%lx, b=0x%lx, cin=0x%lx\n", dut->xclk.clk, i.a,
            i.b, i.cin);
        printf("DUT: sum=0x%lx, cout=0x%lx\n", o_dut.sum, o_dut.cout);
    }

    delete dut; // automatically call dut.finalize() in ~UTAdder()
    printf("Simulation finished\n");
    return 0;
}
Last modified September 12, 2024: Fix typo (4b0984f)