Monday 7 November 2016

UVM Random Stimulus Generator

In verification using UVM, most of the people create sequences from sequence item and start the same sequence from the testcase. In this blog, we will see a different method to drive transactions without creating any sequence. UVM provides “uvm_random_stimulus” class which generates random transactions and put it on to “blocking_put_port”. So by using this port, we can get the random transaction and drive them on to the interface. “uvm_random_stimulus” class contains “generate_stimulus” method whose arguments are the transaction class and number of transaction to be generated. User can override this method to have his own implementation. It also contains “stop_stimulus_generation” function which stops generation of stimulus.

As we know, the integration of any VIP in the testbench is not completed until a sequence is driven from the driver and monitor samples interface properly. When we integrate any VIP, it may consume time in creating sequences and testcase which could be done later. Here if any error is generated, then user has to find whether there is any problem with component integration or it is related to the sequence or testcase. As this random stimulus generator method doesn’t include any sequences, one can easily stabilize the integration of VIP in the testbench. Error can be generated only because of integration problem (Note that, simulation time error will be printed for erroneous transactions. So constraints should be proper.). So, this method of transaction generation is very useful for the AE(application engineer) and also for a new user of the VIP.

There can be various ways of integrating this random stimulus generator:
One way is, you can create an extra port (put implementation) in the driver and connect it with the stimulus generator put port (“blocking_put_port”). In run phase, get the transaction either from put implementation or seq_item_port based on configuration. In that case, driver contains two ports for getting transactions and it may break the existing functionality.

Another way is, you can override the “get_next_item” method in the sequencer. So without updating the driver code, you can easily integrate the stimulus generator in the testbench. You just need to configure the agent whether to get transaction from sequences or from stimulus generator. Below is the demo example which demonstrates usage of “uvm_random_stimulus” component:
Random Stimulus Generator


// Enum to select transaction generation from sequence or random stimulus generator.
typedef enum 
{
    RANDOM_GENERATOR,
    SEQUENCE_GENERATOR
} txn_generator;

//********************
// Transaction class 
class a_item extends uvm_sequence_item;
    ...  
endclass : a_item

//********************
// Driver class
class a_driver extends uvm_driver#(a_item);
    ... 
    task run_phase(uvm_phase phase);
        super.run_phase(phase);
        forever begin
          seq_item_port.get_next_item(req);
          ...
          seq_item_port.item_done();
        end
    endtask : run_phase
endclass : a_driver

//********************
// Sequencer class
`uvm_blocking_put_imp_decl(_rand)
class a_seqr extends uvm_sequencer#(a_item);
    txn_generator gen=SEQUENCE_GENERATOR;
    uvm_blocking_put_imp_my #(a_item, a_seqr) put_imp;
    ...
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        void'(uvm_config_db#(txn_generator)::get(this,"","gen",gen));
        // Creating implementation port when RANDOM_GENERATOR is selected
        if(gen == RANDOM_GENERATOR)
            put_imp = new("put_imp", this);
    endfunction : build_phase

    // Put implementation
    virtual task put_rand(a_item seq);
        if(seq == null) $display("ERROR: TXN NULL");
        else m_req_fifo.put(seq);
    endtask

    task get_next_item(output a_item t);
        a_item req_item;
  
        if (get_next_item_called == 1)
            uvm_report_error(get_full_name(),
            "Get_next_item called twice without item_done or get in between", UVM_NONE);

        // Calling m_select_sequence method only when transaction from sequences
        // are required. This is the only modification. 
        // Other code remains same as base implementation.
        if ((!sequence_item_requested) && (gen == SEQUENCE_GENERATOR))
            m_select_sequence();

        // Set flag indicating that the item has been requested to ensure that item_done 
        // or get is called between requests
        sequence_item_requested = 1;
        get_next_item_called = 1;
        m_req_fifo.peek(t);
    endtask
endclass : a_seqr

//********************
// Agent class
class a_agent extends uvm_agent;
    a_driver drv;
    a_seqr   seqr;
    a_item   item;
    txn_generator gen=SEQUENCE_GENERATOR;
    uvm_random_stimulus #(a_item) rand_txn_generator;
    int num_rand_txn=20;
    ...
    function void build_phase(uvm_phase phase);
        ...
        // Getting transaction generator
        void'(uvm_config_db#(txn_generator)::get(this,"","gen",gen));
        // Setting transaction generator to sequencer
        uvm_config_db#(txn_generator)::set(this,"seqr","gen",gen);
        // Getting how many transaction should be generated from random stimulus
        // generator.
        uvm_config_db#(int)::get(this,"","num_rand_txn",num_rand_txn);

        `uvm_info("build_phase", $sformatf("Transaction generator is:%0s",gen.name()), UVM_LOW)
        drv  = a_driver::type_id::create("drv", this);
        seqr = a_seqr::type_id::create("seqr", this);

        // Creating random stimulus generator
        if(gen == RANDOM_GENERATOR)
        begin
      rand_txn_generator = uvm_random_stimulus#(a_item)::type_id::create ("rand_txn_generator", this);
          item = a_item::type_id::create("item");
        end
    endfunction : build_phase

    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        drv.seq_item_port.connect(seqr.seq_item_export);
        if(gen == RANDOM_GENERATOR)
            rand_txn_generator.blocking_put_port.connect(seqr.put_imp);
    endfunction : connect_phase
  
    task run_phase(uvm_phase phase);
        super.run_phase(phase);
        if(gen == RANDOM_GENERATOR)
        begin
            phase.raise_objection(this);
            rand_txn_generator.generate_stimulus(item, num_rand_txn);
            phase.drop_objection(this);
        end
    endtask : run_phase
endclass : a_agent

//********************
// Environment class
class m_env extends uvm_env;
    a_agent a_ag;
    ...  
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        a_ag = a_agent::type_id::create("a_ag", this);
        ...
    endfunction : build_phase
    ...
endclass :m_env

//********************
// Testcase 
class test extends uvm_test;
    m_env env;
    ...
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        uvm_config_db#(txn_generator)::set(this,"env.a_ag","gen",RANDOM_GENERATOR);
        uvm_config_db#(int)::set(this,"env.a_ag","num_rand_txn",50);
        ...
        env = m_env::type_id::create("env", this);
    endfunction : build_phase
endclass : test

As shown in the example, txn_generator is an enum which is used to select transaction generation from random stimulus generator or from sequences. Here, a_item is a sequence item class and a_driver is a driver class. Sequence item class and driver class implementation doesn't require any extra modification. 

a_seqr is a sequencer class which contains "put_imp" implementation port. When txn_generator is set to RANDOM_GENERATOR, transaction will be put into m_req_fifo from random stimulus generator otherwise it will be put from sequences. Note that, get_next_item method is overridden in the sequencer class. "m_select_sequence" method is called only when txn_generator is set to SEQUENCE_GENERATOR.

In a_agent class, rand_txn_generator is created which is a random stimulus generator component and it's "blocking_put_port" is connected with "put_imp" of sequencer class. In run phase, "generate_stimulus" method is called which generates "num_rand_txn" number of random transactions.

In the testcase, we are just setting "txn_generator" to "RANDOM_GENERATOR" and "num_rand_txn" (number of random transaction to be generated) to 50. So, 50 random transaction will be generated.

This way, without using any sequence user can generate random transactions and drive them on to interface. So, VIP integration task will become much simple.

Note: You may get fatal ("Driver put a response with null sequence_id") if driver is calling "put_response" task for putting response. To avoid it, you can compile with"+define+CDNS_NO_SQR_CHK_SEQ_ID".

Saturday 1 October 2016

Build an UVM scoreboard in few minutes

If you feel doing repetitive coding is a boring task, then this blog is for you! UVM provides many inbuilt features which can be easily plugged in to our testbench with little modification. So, we can reduce our coding time and achieve our goal of verification. In this post, we will see how the UVM is useful for building a scoreboard quickly.

Generally, scoreboard has a function for creating analysis ports (expected transaction and actual transaction) which is connected with transmitter's analysis port and receiver's analysis port, a function which converts a transaction class into another transaction class, FIFOs for storing transactions and methods for getting transaction and compare them. In most of the projects, we are using the same kind of scoreboard structure and mostly transaction conversion method differs from project to project. 

UVM provides all these features in "uvm_algorithmic_comparator" class and you can easily plug-n-play the scoreboard component in your environment. Just, you need to concentrate on writing a method which translates a transaction class into another type of transaction class. So, you can easily save your time in terms of coding and compiling the scoreboard code. 

Below is the example of "uvm_algorithmic_comparator".

Step (1): Create a transformer class and overwrite "transform" method which converts an input transaction class into another type of (output) transaction class.
In this example, "m_transformer" class is created and "a_item" transaction class is converted into "b_item" transaction using "transform" method in it.

class m_transformer extends uvm_component;
    ...  
    function b_item transform(a_item a_i);
        b_item b_i;
        b_i      = new("b_i");
        b_i.b   = a_i.a;
        b_i.bb = a_i.aa;
        return b_i;
    endfunction
endclass : m_transformer

Step (2): Instantiate the transformer class into "environment" class along with other agents in "uvm_algorithmic_comparator" class.
"uvm_algorithmic_comparator" has three parameters:
BEFORE: A transaction class which needs to be converted 
AFTER:   A transaction class which needs to be compared 
TRANSFORMER: A component which contains "transform" method

Note that, you must have to implement "convert2string" and "do_compare" method in ~AFTER~ transaction class.

Step (3): Connect analysis port having ~BEFORE~ transaction class with "before_export" port of algorithmic comparator and analysis port having ~AFTER~ transaction class with "after_export" port.

class m_env extends uvm_env;
    // Agent class handle
    a_agent a_ag;
    b_agent b_ag;
    // STEP - 2
    uvm_algorithmic_comparator #(a_item, b_item, m_transformer) algo;
    m_transformer transf;

    ...  

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        transf = new("transf",this);
        algo   = new("algo",this, transf);
        a_ag  = a_agent::type_id::create("a_ag", this);
        b_ag  = b_agent::type_id::create("b_ag", this);
    endfunction : build_phase
  
    // STEP - 3
    function void connect_phase(uvm_phase phase);
        a_ag.drv.a_ap.connect(algo.before_export);
        b_ag.drv.b_ap.connect(algo.after_export);
    endfunction : connect_phase
endclass :m_env


Alright, you are done with the scoreboard and good to go for simulation!!!

Here, when "a_ag.drv" broadcast any transaction on "a_ap" analysis port, it will be converted in to "b_item" transaction class and stored in "m_before_fifo" tlm analysis fifo of "uvm_in_order_comparator" class. Similarly, when "b_ag.drv" broadcast any transaction on "b_ap" analysis port, it will be stored in "m_after_fifo" tlm analysis fifo of "uvm_in_order_comparator" class. In run phase, when any transaction available in "m_before_fifo" and "m_after_fifo", they will be compared.

Now, when any transaction mismatch, this scoreboard will give a warning (UVM_WARNING) message "Comparator Mismatch" that you can override to error (UVM_ERROR) message based on your project need.




Wednesday 28 September 2016

Synchronization using uvm_objection

In this blog, we will see how to do synchronization using uvm_objection.

UVM provides "raise_objection" method for raising objections and "drop_objection" method for dropping objections in the testbench. These methods are the part of "uvm_objection" class. We will see, how these methods synchronizes various tasks in the component.

Below is an example of a normal code where an objection is raised and then the two tasks (response and process_txn) are executed in parallel. One can assume that, after getting transaction from "seq_item_port", the driver is executing that transaction and receiving response for the same. In "process_txn" task, two tasks are called in a serial way, "process_header" followed by "process_data". Once execution of "process_header" task is completed, "process_resp" event is triggered which executes "response" task (from waiting state of the same event). In parallel with that, "process_data" task is executed. Once "response" and "process_txn" tasks are completed, an objection is dropped. 

Method 1:
  event process_resp;
  task run_phase(uvm_phase phase);
    ...
    for(int i = 0; i < 10; ++i) 
    begin
      phase.raise_objection(this);
      fork
        response();
        process_txn();
      join
      phase.drop_objection(this);
    `uvm_info("run_phase","Done", UVM_LOW)
    end
    `uvm_info("run_phase","Run phase completed", UVM_LOW)
  endtask : run_phase

  task process_txn();

    process_header();
    -> process_resp;
    process_data();
  endtask : process_txn

  task process_header();

    ...
    #20;
  endtask

  task process_data();

    ...
    #15;
  endtask

  task response();

     ... 
    @(process_resp);
    `uvm_info(get_full_name(),$sformatf("Processing Response"), UVM_LOW)
    #25;
  endtask : response


The same thing can be achieved by using "uvm_objection" methods.

"raise_objection" and "drop_objection" methods has three arguments:
    obj - Handle of the calling class usually "this"
    description - String for indicating specific objection
    count - Raise or drop number of objection, default value is 1.

Whenever "raise_objection" method is called, by default it raise one objection and one objection is dropped while calling "drop_objection" method. On 3rd argument, we can pass any number which raise/drop that many objections. 

UVM objection class has two methods which helps you to synchronize the processes based on these objections. You can wait on objection raise/drop event or on objection count value. Below are the two methods:

(1) "wait_for" method is used for event based waiting. Event can be raising an objection, dropping an objection or dropping all objections.  It has two arguments:
    objt_event - It is an enum having values "UVM_RAISED" (triggers when an objection raised), "UVM_DROPPED" (triggers when an objection dropped) and "UVM_ALL_DROPPED" (triggers when all objection dropped). 
    obj - Handle of uvm_object class on which triggering of event depends

(2) "wait_for_total_count" method is used for waiting until objection count reaches to a specific value. It also has two arguments:
    obj - Handle of uvm_object class usually this 
    count - Wait until the objection count of the obj object reaches to this value

By using these features, above example code could be written in following way:

Method 2:
  uvm_objection obj;
  task run_phase(uvm_phase phase);
    obj = phase.get_objection();
    for(int i = 0; i < 10; ++i) 
    begin
      phase.raise_objection(this, "Rasing Objection", 2);
      fork
        response();
        process_txn(phase);
      join
      phase.drop_objection(this);
    `uvm_info("run_phase","Done", UVM_LOW)
    end
    `uvm_info("run_phase","Run phase completed", UVM_LOW)
  endtask : run_phase

  task process_txn(uvm_phase phase);

    process_header();
    phase.drop_objection(this);
    process_data();
  endtask : process_txn

  task process_header();

    ...
    #20;
  endtask

  task process_data();

    ...
    #15;
  endtask

  task response();

    ...
    obj.wait_for(UVM_DROPPED, this) // or "obj.wait_for_total_count(this, 1);"
    `uvm_info(get_full_name(),$sformatf("Processing Response"), UVM_LOW)
    #25;
  endtask : response

Note that, 2 objections are raised in the above example. An objection is dropped after executing "process_header" task. So, total raised objection count is set to 1. In "response" task, "wait_for" method is used for waiting on objection drop event in this class ("wait_for_total_count" method is used for waiting objection count reaches to 1). So, it starts execution in parallel to "process_data" task. Once "response" and "process_txn" tasks are completed, an objection will be dropped.  

Here, both methods give same output result.

Thursday 2 June 2016

Be careful while writing UVM Report Messages


This blog is dedicated to helping you with implementing UVM message report mechanism in your testbench. During verification, everybody used to run multiple regressions to generate different scenarios. Most cases, we find errors while running regression which are reported as UVM_ERROR in the log file.

Due to limitation of time-line, we prefer to run the regression with “UVM_NONE” verbosity. But there may be a case when this regression result is not reliable.

Take an example, you have implemented a function for “crc_check” which checks the CRC value is expected or not. If it is not expected then prints an error message that “Received CRC is incorrect”. Here, some people prefer UVM macros for printing messages i.e.
`uvm_error(“crc_check”, “Received CRC is incorrect”)

But some people do not prefer to use macros. So they use UVM functions (uvm_report_*) for message reporting. So, we may write:
uvm_report_error(“crc_check”, “Received CRC is incorrect”);

There is no problem in NORMAL simulation. But a big problem arises when we simulate the test with UVM_NONE verbosity. Here is the problem:

Sometimes we become habitual by using “`uvm_error” macro in which verbosity is not required. UVM macro take care it internally. Note: Verbosity of `uvm_error is “UVM_NONE”. In “uvm_report_error” function, 3rd argument specifies the verbosity of that error message. Default value of that argument is “UVM_LOW”. In the above case, verbosity is not specified and running simulation with “UVM_NONE” verbosity. So, UVM will not report any error message in the log files and also in the summary message, you will not get any UVM_ERROR.

So, Congratulation!!! Your Test PassedJ.

But is it really passed? Or did you forget something in a hurry? Or Anyhow you want to make it pass? J

So, proper usage of “uvm_report_error” should be,

uvm_report_error(“crc_check”, “Received CRC is incorrect”, UVM_NONE);

Sunday 24 April 2016

Usage of sub-phases and phase jump in UVM


In this blog, we will see usage of UVM sub-phases by taking an example of reset test. As we know, in UVM, run_phase is divided into multiple sub-phases:

  1. pre_reset_phase(), reset_phase(), post_reset_phase(): Phases involved in reset activity.
  2. pre_configure_phase(), configure_phase(), post_configure_phase(): Phases involved in configuring DUT.
  3. pre_main_phase(), main_phase(), post_main_phase(): Phases involved in driving main stimulus to the DUT.
  4. pre_shutdown_phase(), shutdown_phase and post_shutdown_phase(): Phases involved in settling down the DUT after driving main stimulus.

In every verification environment, reset testing is a crucial feature and in many cases, we find issues. In it, user drives random sequence to the DUT and in between data transmission, reset is applied followed by driving restart sequence. So, we will see how the reset testing could be easily tested using sub-phases and “jump” feature of UVM.

Figure 1

As shown in Figure 1, test should wait to reset de-asserted and then it shall drive reset sequence (if defined). In post reset phase, it should check any signals status like interrupt based on reset sequence. Also, it is good to check the reset values of the signals in the pre_reset_phase() which is not shown here.

Figure 2

As shown in Figure 2, driver should drive reset value of the signals in the reset phase and then it waits for the reset de-asserted. It should clean all the queues and array in the pre_reset/reset phase and also resets the configurations. Same thing shall be applicable to the other components like monitor and a scoreboard.

If queues/arrays will not be cleared, then you may face unwanted issues like scoreboard mismatch or transaction field error during simulation. Many of us have faced these kinds of issues because of these small mistakes in the testbench.

Then it comes to configure phase. In pre_configure phase, user can set the configuration fields in the different testbench component and in configure phase, the DUT initialization sequence could be driven which enables DUT to run the traffic on the bus.

As shown in Figure 1, in the main phase of the testcase, we can drive the main sequence which contains main traffic which DUT samples and responds. Also, it includes a useful feature of “phase jumping”, which could be used while testing the reset functionality. By using this feature, we can jump back to reset phase from the main phase where we apply the reset. So that, all component’s phases will be set to the reset phase and all fields will have their default values.

However, user needs to take care many things while using this phase jumping feature like if any component (like a driver/monitor) doesn’t implement these sub phases properly and all traffic is driven from run_phase then it creates a big problem for the user.

In the shutdown phase, user can drive sequence like sleep sequence or somewhat else.

Note: This coding practice is not mandatory, but it’s just an example. It is not good to use a phase jumping feature in case any of the components of testbench don’t use the sub-phases of UVM.


Reference:
  1. Universal Verification Methodology (UVM) 1.2 Class Reference

Sunday 13 March 2016

How to kill a process in SystemVerilog


In this blog, we will see a couple of issues in the simulation while using "disable fork" and "disable LABEL" statements. Most of us, have faced these kind of issues at least one time in our system verilog programming and I just want to sum up them on a single page.


(1) Issue while using "disable fork;" statement in the code

Below is the pseudo code to understand:

task multiple_process();
    fork
        forever begin
            #10; // Task-A
            $display("Process-1");
        end
    join_none
    fork
    begin
        dly1=$urandom_range(1,10);
        #dly1; // Task-B
        $display("Process-2");
    end
    begin
        dly1=$urandom_range(1,20);
        #dly1; / Task-C
        $display("Process-3");
    end
    join_any
    disable fork;
    $display("Process-4");
endtask : multiple_process

As shown in above task, "Task-A" process is running continuously. Two different processes, "Task-B" and "Task-C" should run in parallel and any of two processes is completed, other process shall stop its execution due to join_any statement.

While simulating above code, you may face that when the disable fork is executed, "Task-A" process also stop it's execution. So, to avoid this kind of situation, it's better to use "disable LABEL" statement.

(2) Issue while using "disable LABEL" statement:

task multiple_process();
    fork
        forever begin
            #10; // Task-A
            $display("Process-1");
        end
    join_none

    fork : LABEL_B_C

    begin
        dly1=$urandom_range(1,10);
        #dly1; // Task-B
        $display("Process-2");
    end
    begin
        dly1=$urandom_range(1,20);
        #dly1; / Task-C
        $display("Process-3");
    end
    join_any
    disable LABEL_B_C;
    $display("Process-4");
endtask : multiple_process


Above code works fine with a single instance of that class. It disables only "Task-B" and/or "Task-C" process when "disable LABEL_B_C" is executed. But when there are multiple instances of the same class in the testbench and all the instances are executing their threads simultaneously then the simulation will stop after executing "disable LABEL_B_C" statement of any instance.


To handle these kind of situations, it's better to use "process" class in the system verilog to kill any process.

task multiple_process();
    process b_process, c_process;

    fork

        forever begin
            #10; // Task-A
            $display("Process-1");
        end
    join_none

    fork : LABEL_B_C

    begin 
        b_process = process::self();
        dly1=$urandom_range(1,10);
        #dly1; // Task-B
        $display("Process-2");
    end
    begin
        c_process = process::self();
        dly1=$urandom_range(1,20);
        #dly1; / Task-C
        $display("Process-3");
    end
    join_any
    if(b_process.status != process::FINISHED)
        b_process.kill();
    if(c_process.status != process::FINISHED)
        c_process.kill();

    $display(“Process-4”);
endtask : multiple_process

Here, we are defining each process using process class and after join_any, we are checking that whether any of “b_process” or “c_process” is finished or not.If it is  not finished, then kill that process.

Reference:
(1) IEEE Standard for SystemVerilog (IEEE Std 1800™-2012)
(2) https://verificationacademy.com/forums/systemverilog/fork-within-loop-join-all