Thursday, 2 February 2017

How to disable triggering of an UVM event

As VIP developers, we have to develop our VIP flexible, so that user can achieve his requirement easily without our dependency. For that, user should have support of configurations, callbacks, factory override etc. in the VIP. In this blog, we will see a feature of UVM which helps user to control the data flow or processes.

In my previous blog "Synchronization using UVM features" (Feb'16), we saw that, how to use UVM events and barrier for synchronization of processes. In this blog, we will see how to disable the triggering of any UVM event which gives control to user by avoiding further processing of data/packet.

As a testbench component developer, we may use the UVM events for synchronization of processes for example once packet is driven properly from driver and relative response is received then triggers an event which do further processing and put that packet on to the analysis port for scoreboarding or coverage sampling. This driver component may be used by number of people and as a VIP developer we don’t know their requirements.

Consider a case where a user don’t want to receive any erroneous packet in scoreboard or coverage. Here, we can provide support for user to disable the triggering of event in his testbench. So that, packet will not be broadcasted on analysis port. This can be achieve using configuration also. But it requires extra variables in the testbench.

To disable triggering of UVM event, UVM event callback class is required. User has to register an UVM event callback class with the specific event. In that callback class, user can override the “pre_trigger” method to disable the triggering of the event. If this method returns "1" then event will not be triggered otherwise it will be triggered. This method is called before triggering of any event.
This method has two arguments:
(1) uvm_event: The event which is calling this callback method
(2) uvm_object: UVM Object class can be passed with the triggering of the event

Below is the example to disable the triggering of UVM event which demonstrate the usage.

Example: module top();
  ...
  uvm_barrier b;

  class my_object extends uvm_object;
    int cntr;
    ...
   endclass

  class my_event_callback extends uvm_event_callback;
  
    ... 
    virtual function bit pre_trigger (uvm_event e, uvm_object data);
      my_object obj;
      $cast(obj, data);
      if(obj.cntr >= 3) return 1;
      else return 0;
    endfunction
  endclass

  // Task - 1
  task task_a(input uvm_barrier _b);
    int delay;

    repeat(5) begin
      delay = $urandom_range(10, 100);
      #delay;
      _b.wait_for();
    end
  endtask : task_a

  // Task - 2
  task task_b(input uvm_barrier _b);
    int delay;

    repeat(5) begin
      delay = $urandom_range(50, 100);
      #delay;
      _b.wait_for();
    end
  endtask : task_b

  task get_event(output uvm_event e_max_process);
    uvm_event_pool e_pool;
    
    if(e_pool == null)
      e_pool = new("e_pool");

    e_pool = e_pool.get_global_pool();
    e_max_process = e_pool.get("e_max_process");
  endtask
  
  initial begin
    // Creates a barrier object named "Barrier" and it's threshold value will
    // be 3.
    b = new("Barrier",3); // Wait for 3 process to be completed
    fork
      task_a(._b(b));
      task_b(._b(b));
      trigger_event(b);
      process_on_event();
    join
  end

  task trigger_event(uvm_barrier b);
      uvm_event e_max_process;
      my_event_callback cb;
      my_object obj;
      int cntr;

      obj = new();
      cb = new();
      get_event(e_max_process);
      e_max_process.add_callback(cb);
      forever begin
        b.wait_for(); 
        #0;
        ++cntr;
        obj.cntr = cntr;
        $display($time," e_max_process[%0d] event being triggered",b.get_threshold());
        e_max_process.trigger(obj);
        #10;
        $display($time," e_max_process event[%0d] reset being done",b.get_threshold());
        e_max_process.reset();
     end
 endtask

  task process_on_event();
    uvm_event e_max_process;

    get_event(e_max_process);
    forever begin
      e_max_process.wait_trigger();
      $display($time," e_max_process event triggered");
      e_max_process.wait_off(); 
    end 
  endtask
endmodule

As shown above, there is an event "e_max_process" which is triggered (in trigger_event task) once synchronization done between "task_a" and "task_b" through "uvm_barrier" followed by resetting the same event. In "process_on_event" task, first it waits for the triggering of same event and followed by waiting on resetting it.

With "e_max_process" event, "my_event_callback" class is registered and with triggering  of the event object of "my_object" class is passed as an argument. So before triggering this event, "pre_trigger" method of callback class is called and it has data of "my_object" passed with the event. 

In the "pre_trigger" method, when the "cntr" field of "my_object" is greater than or equals to 3 then it returns 1 otherwise it returns 0. That means, when cntr value is greater than or equals to 3, the event will not be triggered. This "cntr" field is incremented to one before triggering of the event.

Below is the simulation result of the example which shows "e_max_process" event is triggered on 85ns and 185ns i.e. 2 times it triggered. Then it is not triggered as "pre_trigger" function returns 1.

Output:
    85 e_max_process[3] event being triggered
    85 e_max_process event triggered
    95 e_max_process event[3] reset being done
   185 e_max_process[3] event being triggered
   185 e_max_process event triggered
   195 e_max_process event[3] reset being done
   268 e_max_process[3] event being triggered
   278 e_max_process event[3] reset being done
   368 e_max_process[3] event being triggered
   378 e_max_process event[3] reset being done
   447 e_max_process[3] event being triggered
   457 e_max_process event[3] reset being done




Monday, 2 January 2017

How to use Parameters in SystemVerilog

In this blog, we will see how to set and overwrite the parameters in the testbench. As we know, whenever changes required in define, we have to compile the code before simulate it. But in case of parameters, we can directly pass its value through simulation time argument and we don't need to compile the code again.


Consider case of AMBA protocols where you may need to have different address/data width and also need to generate data based on the width of the signal. So here, we can use parameter to generate different values.


For overwriting the parameter field, user has to pass “+floatparameters” during optimization [with vopt command]. If the design is not optimized then similar command could be passed with simulation time [with vsim command]. The parameter field should be passed during simulation time with “-g” prefix.

Below is a demo example which has a parameter “DATA_WIDTH” which defines the width of signals. 

Below is the example for usage of parameter with Questa-Sim.


Example:
module adder(in1, in2, en, clk, out);
  parameter DATA_WIDTH=4;
  output [DATA_WIDTH : 0] out;
  input [(DATA_WIDTH-1):0] in1;
  input [(DATA_WIDTH-1):0] in2;
  input en;
  input clk;

  reg [DATA_WIDTH:0] r_out;

  assign out = r_out;

  always @(posedge clk)
  begin
    if (en) begin
      r_out <= in1 + in2;
    end
  end
endmodule

module testbench();
  parameter DATA_WIDTH=2;
  wire  [DATA_WIDTH : 0]  out;
  wire [(DATA_WIDTH-1):0] in1;
  wire [(DATA_WIDTH-1):0] in2;
  wire en;
  wire clk;

  reg [(DATA_WIDTH-1):0] r_in1;
  reg [(DATA_WIDTH-1):0] r_in2;
  reg r_en;
  reg r_clk;

  assign clk = r_clk;
  assign en  = r_en;
  assign in1 = r_in1;
  assign in2 = r_in2;

  initial begin
    r_clk <= 0;
    $monitor(in1, in2, out);
    forever begin
      #5 r_clk = ~r_clk;
    end
  end

  initial begin
    r_en <= 0;
    r_in1 <= 0;
    r_in2 <= 0;

    #5 r_en = 1;
    #10 r_in1 = $urandom_range(128,0);
          r_in2 = $urandom_range(128,0);
    #10 r_in1 = $urandom_range(128,0);
           r_in2 = $urandom_range(128,0);
    $finish;
  end
  
  adder#(DATA_WIDTH) add(in1, in2, en, clk, out);

endmodule



Compile command: vlog file.sv


The following command should be used to simulate above example:
vsim -novopt -do "log -r *;run -a" testbench -voptargs="+floatparameters" -gDATA_WIDTH=6 –c


So, it will take "DATA_WIDTH" value "6" in the entire testbench. User can pass different values of "DATA_WIDTH" with "-g" option. If this parameter is not passed during simulation time, then "DATA_WIDTH" value will be "2" which is set as default in the testbench.



Note that, by this method, the parameters defined in the packages will not be overridden. So that, the testbench component can not use this overriden value.




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);