Skip to content

quic/SvUTest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

SvUTest: A Unit Testing Framework in System Verilog

System Verilog Unit Testing Framework (SvUTest) is an open source framework for performing unit testing of lightweight hardware modules written in System Verilog. This project is inspired by CppUTest, Google Test and UVM.

Introduction

Large digital designs often contain hundreds of blocks organized into clusters, with verification done typically done at block, cluster and top levels, generally using UVM. A single block, can itself be quite large, consisting of anywhere between a handful to a hundred modules. Testing of these lower level modules is sometimes left to the block-level (or higher) verification environment, which results in a longer turnaround. Building UVM testbenches for each lower-level module is generally not practical. While not impossible, this approach has issues that a UVM testbench can only run one test at a time and there's no inbuilt mechanism to consolidate results of multiple runs.

SvUTest is an attempt at a tool that helps designers write sanity checks on their building blocks with minimal overhead. With support for concurrent regressions and inbuilt consolidation of results, this framework enables faster design sign-off.

Target Audience

SvUTest is meant to be used by RTL design engineers to ensure basic sanity of small designs across a known set of input patterns. This framework not meant to be a replacement for UVM and is only recommended for small System Verilog modules with a handful of input/output interfaces and a set of input workloads whose output is known. UVM would still be the go-to solution for large designs with complex stimuli.

Building a unit test

Building a unit test requires creating a module that contains the Design Under Test (DUT), called the test_top, a test_case class that describes the inputs and outputs to the DUT and finally top level module called regress_top that creates and runs a test_list.

Test Top

Let's look at the floating-point multiplier block in examples/001_floatmul that we need to unit-test. This DUT has two input channels and an output channel, all following the valid-data-ready protocol:

input  logic        a_valid,
input  float32_t    a_payload,
output logic        a_ready,

input  logic        b_valid,
input  float32_t    b_payload,
output logic        b_ready,

output logic        o_valid,
output float32_t    o_payload,
input  logic        o_ready,

The valid on the output interface shall be asserted if and only if both the inputs' valids a_valid and b_valid are high.

The test_top is a System Verilog module with a single interface port of type svutest_test_ctrl_if and a single type parameter (T_test_case):

module floatmul_test_top
    // import any other packages needed by the DUT
    import svutest_pkg::*;
#(
    type T_test_case = bit  // Need to be overriden during instantiation
)(
    svutest_test_ctrl_if.target tc
);
    ...
endmodule

This type parameter must be a class derived from svutest_pkg::test_case class.

The test_top needs to instantiate the following items:

  • A done signal to indicate that the DUT has finished operation. This signal may be driven by the DUT or may be generated by the test_top. In this example, done is driven from the test_top.
  • An instance of svutest_dut_ctrl_if. This interface supplies the clock and reset to the interior of the test_top and collects the done signal. It is driven from the run() method of the test_case class instance
  • An instance of svutest_req_payload_rsp_if interface for each channel on the DUT. These interfaces supply the input transactions to and collect the output transactions from, the DUT. svutest_req_payload_rsp_if is a generic interface that is used to emulate valid..data..ready protocol ampng others. svutest_req_payload_rsp_if need to be replaced with the right interface in case the protocol is different from valid-data-ready.
  • An instance of the DUT, with the ports on the DUT hooked to the signals from the svutest_dut_ctrl_if and svutest_req_payload_rsp_if interfaces.
  • An initial .. begin block where the T_test_case parameter is instantiated, and its run() method is called. The constructor of T_test_case must accept the svutest_test_ctrl_if and svutest_dut_ctrl_if interface instances in addition to other interface instnaces declared inside the test_top. After the call to the constructor, the run() method of the test_case class must be called to start the test.

With the above instantiations in place, the test_top is now complete:

module floatmul_test_top
    import floatmul_pkg::*;
#(
    type T_test_case = bit
)(
    svutest_test_ctrl_if.target tc
);
    svutest_dut_ctrl_if dc ();
    
    svutest_req_payload_rsp_if#(float32_t) a (dc.clk, dc.rst);
    svutest_req_payload_rsp_if#(float32_t) b (dc.clk, dc.rst);
    svutest_req_payload_rsp_if#(float32_t) o (dc.clk, dc.rst);
    
    logic busy;
    
    // ---------------------------------------------------------------------- //
    
    floatmul u_fmul (
        .clk        (dc.clk),
        .rst        (dc.rst),
        .busy       (busy),
        
        .a_valid    (a.valid),
        .a_payload  (a.payload),
        .a_ready    (a.ready),
        
        .b_valid    (b.valid),
        .b_payload  (b.payload),
        .b_ready    (b.ready),
        
        .o_valid    (o.valid),
        .o_payload  (o.payload),
        .o_ready    (o.ready)
    );
    
    always_comb dc.done = ~(a.valid | b.valid | o.valid | busy);
    
    // ---------------------------------------------------------------------- //
    
    initial begin
        T_test_case test = new(tc, dc, a, b, o);
        test.run();
    end
endmodule

A test_top is built once per DUT.

test_case class

Once the test_top is built, a test_case class that drives the input interfaces and evaluates the output transactions needs to be created by deriving from svutest_pkg::test_case. This class acts as a base class for all test sequences for the current DUT. svutest_pkg::test_case class drives the clock and reset to the DUT while monitoring the done in addition to managing the input and output interfaces of the DUT. The constructor of svutest_pkg::test_case accepts the svutest_test_ctrl_if interface, svutest_dut_ctrl_if interface and a test name as constructor arguments:

class floatmul_utest extends test_case;
    valid_data_ready_injector#(float32_t) m_a_injector;
    valid_data_ready_injector#(float32_t) m_b_injector;
    valid_data_ready_extractor#(float32_t) m_o_extractor;
    
    function new (
        virtual svutest_test_ctrl_if.target vif_test_ctrl,
        virtual svutest_dut_ctrl_if vif_dut_ctrl,
        virtual svutest_req_payload_rsp_if#(float32_t).driver vif_a,
        virtual svutest_req_payload_rsp_if#(float32_t).driver vif_b,
        virtual svutest_req_payload_rsp_if#(float32_t).target vif_o,
        string test_case_name
    );
        super.new(vif_test_ctrl, vif_dut_ctrl, $sformatf("fmul:%0s", test_case_name));
        
        m_a_injector = new(vif_a);
        m_b_injector = new(vif_b);
        m_o_extractor = new(vif_o);
        
        this.add(m_a_agent);
        this.add(m_b_agent);
        this.add(m_o_agent);
    endfunction
endclass

sv_utest_pkg provides injectors and extractors for 2 different interface protocols:

  1. valid_data_injector: Data with valid
  2. valid_data_ready_injector: Data with valid and ready

An injector or extractor needs to be created per interface, depending on the direction of the transaction. The example above creates two injectors and an extractor inside the class. The constructor of test_case must attach each injector/extractor to the test case by calling add() method.

Once the injectors and extractors are set up, the user needs to extend the base class for each test scenario and override two virtual functions test_case::populate() and test_case::check(). populate() is used to populate the input data for all the injectors. The injector class provides a function put() to push a transaction its internal queue. Any number of put() calls may be made from the populate function. The actual injection of the transactions to the DUT will be done later.

The transactions emitted from the output channels of the DUT are collected by the extractor and populated into an internal queue. This queue can be accessed from the virtual function check() using get_queue() method and queried for correctness of output transactions. Two macros UTEST_ASSERT(expr) and UTEST_ASSERT_EQ(expr_lhs, expr_rhs) are provided in svutest_defines.svh to help the user with the line number and a failure count summary.

The following snippet shows how a simple test scenario with one transaction on each input interface:

class floatmul_test2_0_0 extends floatmul_utest;
    function new (
        virtual svutest_test_ctrl_if.target vif_test_ctrl,
        virtual svutest_dut_ctrl_if.driver vif_dut_ctrl,
        virtual svutest_req_payload_rsp_if#(float32_t).driver vif_a,
        virtual svutest_req_payload_rsp_if#(float32_t).driver vif_b,
        virtual svutest_req_payload_rsp_if#(float32_t).target vif_o
    );
        super.new(vif_test_ctrl, vif_dut_ctrl, vif_a, vif_b, vif_o, "0_0");
    endfunction
    
    function void populate ();
        m_a_injector.put('{ sign: 1'b0, exponent: '0, mantissa: '0 });
        m_b_injector.put('{ sign: 1'b0, exponent: '0, mantissa: '0 });
    endfunction
    
    function void check ();
        float32_t queue [$] = m_o_extractor.get_queue();
        
        `UTEST_ASSERT_EQ(queue.size(), 1)
        
        `UTEST_ASSERT_EQ(queue[0], '1)
    endfunction
endclass

Regression top module

Once the test_top and test_cases are set up, we need to populate a top module where we do the following:

  1. Create an instance of svutest_test_ctrl_if and the test_top for each test scenario
  2. Create a instance of test_list and add to it all the svutest_test_ctrl_if instances that we created in step 1
  3. Call the run() method on the test list
module regress_top;
    import svutest_pkg::*;
    import floatmul_test_pkg::*;
    
    svutest_test_ctrl_if i_floatmul_test2_0_0 ();
    floatmul_test_top#(floatmul_test2_0_0) u_floatmul_test2_0_0 (i_floatmul_test2_0_0);
    
    svutest_test_ctrl_if i_floatmul_test2_012_012 ();
    floatmul_test_top#(floatmul_test2_012_012) u_floatmul_test2_012_012 (i_floatmul_test2_012_012);
    
    initial begin
        test_list list = new();
        
        list.add(i_floatmul_test2_0_0);
        list.add(i_floatmul_test2_012_012);
        
        list.run();
    end
endmodule

The list.run() method runs all instantiated test cases, in no specific order, and prints a summary (pass +color to the simulation environment for print in color) on the console:

001_floatmul/floatmul_test_pkg.sv", 109: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_8: started at 15000s failed at 15000s
        Offending '(this.m_o_agent.m_mon_queue[3].payload === float32_t'{sign:0, exponent:0, mantissa:0})'
       15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x7f000000, right == 0x0
"001_floatmul/floatmul_test_pkg.sv", 111: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_12: started at 15000s failed at 15000s
        Offending '(this.m_o_agent.m_mon_queue[5].payload === float32_t'{sign:0, exponent:128, mantissa:0})'
       15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x7f000000, right == 0x80000000
"001_floatmul/floatmul_test_pkg.sv", 112: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_14: started at 15000s failed at 15000s
        Offending '(this.m_o_agent.m_mon_queue[6].payload === float32_t'{sign:0, exponent:0, mantissa:0})'
       15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x80000000, right == 0x0
"001_floatmul/floatmul_test_pkg.sv", 114: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_18: started at 15000s failed at 15000s
        Offending '(this.m_o_agent.m_mon_queue[8].payload === float32_t'{sign:0, exponent:129, mantissa:0})'
       15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x80000000, right == 0x81000000
       15000 | fmul:0_0> DONE, PASS (2 / 2)
       15000 | fmul:012_012> DONE, FAIL (6 / 10)
       15000 | Status: DONE, FAIL | (Done: 2, Timeout: 0), (Pass: 1, Fail: 1, Unknown: 0)

Any number of test lists may be created, based on compile time or run time conditions.

Compiling and running

SvUTest framework contains 5 source files:

src/svutest_if.sv
src/svutest_core_pkg.sv
src/svutest_injector_pkg.sv
src/svutest_extractor_pkg.sv
src/svutest_test_pkg.sv 
src/svutest_pkg.sv

and a header file

src/defines.svh

The source files must be compiled in the order specified above by the user's eda tool like while the header file is typically picked up by providing the include path. A typical invocation from the command line would be:

<tool> src/svutest_if.sv src/svutest_core_pkg.sv src/svutest_injector_pkg.sv src/svutest_extractor_pkg.sv src/svutest_test_pkg.sv src/svutest_pkg.sv <include_path_flag> src/ path_to_other_files

Warning

This framework is still in pre-alpha stage.

Development

See CONTRIBUTING.md.

Getting in Contact

License

SvUTest is licensed under the BSD-3-clause License. See LICENSE.txt for the full license text.

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published