0
\$\begingroup\$

I run onto a rather confusing situation where I was writing a code for a UVM monitor. This module performs these operations in order:

  1. Listen to channel (virtual interface) until intercepting one or many events on the pin-level.
  2. Fill in a data structure with any values that can be read on the input ports of the interface.
  3. Spawn a child thread to wait for the response and go back to step 1.

Spawning is using the SystemVerilog Fork.. JOIN_NONE construct and it was meant to start a task that only waits for the response at the interface and then keep going with the parent task - WITHOUT having to wait for the spawned task to finish. Inside the FORK JOIN, there is a SystemVerilog Task with input arguments that are passed by value. When the program starts, index=0. When the get_output(data_struct, index) is executed inside the FORK..JOIN, index is still 0. However it changes once inside the spawned task! Please look carefully to where I put my debug prints of the variable index. The first one prints index = 0 and the second one prints index = 1. It is not possible in my opinion that this can happen, because first, variables are passed by value to the task and second index increments after the task is called assuming instructions take place in a sequential order inside the task run_phase. I want to know why does index change inside the spawned task by itself. The tweak I found to maintain the passed value is to add a wait statement of 1ns after the spawned task has been called.

Here is the code:

task run_phase(uvm_phase phase);
            int unsigned index;
            dpu_input_str data_struct;

            //monitoring

            index = 0;

            //wait until NOT reset
            @(posedge vif.rst_n);
            forever begin
                    //listen to channel: wait for any change in the IF signals
                    fork
                            @(vif.ctrl_0);
                            @(vif.ctrl_1);
                            ...
                    join_any
                    disable fork; //kill outstanding waiting processes
                    //create intermediate struct
                    data_struct.ctrl_0        = vif.ctrl_0;
                    data_struct.ctrl_1        = vif.ctrl_1;
                    ..

                    //wait for the output
                    //spawn a task to perform that and get back to channel listening
                    //again
                    fork
                            get_output(data_struct, index);
                    join_none
                    $display("Value of index after calling the task is:  %d", index);
                    //#1;
                   index++;
                end
        endtask: run_phase

    task get_output(input dpu_input_str data_struct,
                                    input int unsigned  index);
            dpu_simple_transaction dpu_tr;
            int unsigned delay_cnt;
            int i;
            //create analysis transaction
            dpu_tr = dpu_simple_transaction::type_id::create
            (.name("dpu_tr_dut_mon"),
             .contxt(get_full_name));

            $display("Value of index inside the task is:  %d", index);

            // fill in data back into analysis transaction
            // pay attention to cast when writing numeric values to enum datatypes

            dpu_tr.ctrl_0        = dpu_control_type'(data_struct.ctrl_0);
            dpu_tr.ctrl_1        = dpu_control_type'(data_struct.ctrl_1);
            ...

            // decide about the delay
            ...
    endtask
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

To help me explain what is happening, I have copied the relevant portion of your code. There is parent thread which is running and which hits this fork .. join_none. When an SV thread encounters a fork, it registers the enclosing begin..end blocks and sequential statements (like the get_output function call here) as threads with the scheduler. But the scheduler does not spawn these threads immediately. Actually it can not -- until the scheduler gets the execution control. At this point the parent thread is executing. The scheduler has to wait till the parent thread yields. The parent thread will yield on encountering any kind of wait statement (or a blocking tlm port read/write etc).

Had you used the fork paired with join or join_any, the parent thread would have yielded immediately. But with join_none, it does not.

So the parent thread continues with execution and only when it hits #1 (if uncommented) it yields, thereby giving a chance to the scheduler to actually spawn the forked thread. And with #1 commented out, index gets incremented before the forked thread gets a chance to get executed. SystemVerilog does provide you with a solution. It involves using the declarative region of the fork construct. See the 2nd code snippet at the bottom.

//spawn a task to perform that and get back to channel listening again
fork
  get_output(data_struct, index);
join_none
$display("Value of index after calling the task is:  %d", index);
//#1;
index++;

With the following code, the declaration ''automatic int _index = index;'' is executed immediately -- along with the registration of the forked thread. For reference see section 9.3.2 of SV 1800-2012 standard. You can omit the enveloping begin and end from the following code, but I think they add clarity to the code.

fork
  automatic int _index = index;
  begin
    get_output(data_struct, _index);
  end
join_none
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.