How to Decouple Threads in SystemVerilog

How to Decouple Threads in SystemVerilog

Posted by

Killing a task in SystemVerilog is relatively easy using the kill() method from the process API. Usually, when we kill a task, we also want to terminate all of its “sub-tasks” it called. But this is not always the case.

In this article I am presenting one way of killing a task via process.kill() API but still keep alive one of its sub-tasks. In other words, I will try to decouple the parent thread from its child thread so that the child thread is not affected by a call to kill() on its parent’s process.


Device Under Test

Let’s imagine that we have a DUT which is doing some data encryption. Encrypting a plain data word takes 128 clock cycles. The DUT has a small register based interface for setting the plain data and starting the encryption. Outputting the encrypted word is done via a simple valid/data interface.

Encrypting plain data 8’hC5 generates the encrypted data 8’hF9

To makes things interesting, our DUT has the registers in the APB reset domain, and its internal encryption engine in the system reset domain.

Reset domains

This means that, even if the registers are reset, encryption can still continue.


What Is the Problem?

Modeling in our environment the encryption process is fairly straight-forward: we use a register callback to detect when REG_CTRL.START is written with value 1 and then we start the encryption task:

//Callback to detect when software starts encryption
class cfs_enc_cbs_sw_encryption_start extends uvm_reg_cbs;
  ...
  virtual function void post_predict(
    input uvm_reg_field  fld,
    input uvm_reg_data_t previous,
    inout uvm_reg_data_t value,
    input uvm_predict_e  kind,
    input uvm_path_e     path,
    input uvm_reg_map    map);
    if(kind == UVM_PREDICT_WRITE) begin
      //Determine that SW wrote 1 to CTRL.START
      if((previous == 0) && (value == 1)) begin
        //Make sure that the encryption engine is IDLE
        if(model.reg_block.status.active.get_mirrored_value() == 0) begin
          //START encryption task in the model
          model.sw_encryption_start(previous, value);
        end
        ....
      end
    end
  endfunction
endclass

In the model component the encryption task looks something like this:

class cfs_enc_model extends uvm_component implements cfs_enc_reset_handler;
  ...
  //Function called when the software started the encryption
  virtual function void sw_encryption_start(bit prev_value, bit next_value);
    fork
      begin
        encrypt();
      end
    join_none
  endfunction
  ...
  //Task for modeling the encryption
  protected virtual task encrypt();
    int unsigned length = 128;
    uvm_reg_data_t data = do_encryption_step(reg_block.plain.data.get_mirrored_value());
        
    void'(reg_block.status.active.predict(1));
        
    for(int i = 1; i < length; i++) begin
      data = do_encryption_step(data);
      @(posedge apb_vif.pclk);
    end
        
    void'(reg_block.status.active.predict(0));
        
    //Send to the scoreboard expected encrypted data
    port_enc_data.write(data);
  endtask
endclass

This simple implementation works in most of the scenarios but it will fail when we trigger an APB reset in the middle of the encryption process. This is because, if we implemented the reset correctly in the APB agent, the encrypt() task will be killed at APB reset.

APB reset during encryption

The APB monitor logic stops its main task, collect_transactions(), at reset:

//Monitor class
class cfs_apb_monitor extends uvm_component implements cfs_apb_reset_handler;
  ...
  //task for collecting all transactions
  virtual task collect_transactions();
    fork
      begin
        process_collect_transactions = process::self();

        forever begin
          collect_transaction();
        end
      end
    join
  endtask

  //function for handling reset
  virtual function void handle_reset(uvm_phase phase);
    if(process_collect_transactions != null) begin
      //Stop the collect_transactions() task and all of its sub-tasks
      process_collect_transactions.kill();
    end
  endfunction
  ...
endclass

If we look at the stack trace we can see that encrypt() task is actually called by the code from collect_transactions():

Simplified stack trace

So when the APB monitor is reset and collect_transactions() is killed, also encrypt() task from our model is killed.

In the next section we will dive straight into the solution. In the last part of this article I will explain the main idea behind the proposed solution.

Solution: How to Use uvm_thread_decoupler?

uvm_thread_decoupler is a simple utility class which allows a task to be decoupled from the process of its caller.

To make things clear, in this section, the term target task refers to the task we want to decouple – in our case it is a task called encrypt().

To integrate uvm_thread_decoupler in your project follow the next steps:

Step #1: Import uvm_thread_decoupler.sv File

Copy the file uvm_thread_decoupler.sv from GitHub into your project and include it in the necessary package:

package cfs_enc_pkg;
  ...
  `include "uvm_thread_decoupler.sv"
  ...
endpackage

Step #2: Create an Instance of the uvm_thread_decoupler Class

Because uvm_thread_decoupler is a child of uvm_component, creating an instance of it must follow the same approach as with any other uvm_component:

class cfs_enc_model extends uvm_component;
  ...
  //Thread decoupler for handling the encrypt() task
  uvm_thread_decoupler encrypt_decoupler;
  ...
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
        
    encrypt_decoupler = uvm_thread_decoupler::type_id::create("encrypt_decoupler", this);
  endfunction
  ...
endclass

Step #3: Connect the Target Task to the Thread Decoupler

We need to inform the uvm_thread_decoupler which task it needs to decouple from its parent’s process. This is done with the help of a TLM blocking put port.

First we need to declare a TLM blocking put port implementation with a relevant suffix. A good choice for the suffix would be the name of the task we want to decouple.

//Declare a blocking put port implementation for the encrypt() task
`uvm_blocking_put_imp_decl(_encrypt)

Next we need to create an instance of this port implementation:

class cfs_enc_model extends uvm_component;
  ...
  //Port connected the the process decoupler associated with encrypt() task
  uvm_blocking_put_imp_encrypt#(uvm_object, cfs_enc_model) port_encrypt_decoupler;
  ...
  function new(string name = "", uvm_component parent);
    super.new(name, parent);
    ...
    port_encrypt_decoupler = new("port_encrypt_decoupler", this);
  endfunction
  ...
endclass

uvm_thread_decoupler knows which task to decouple by calling that task in the put() implementation of the port:

class cfs_enc_model extends uvm_component;
  ...
  //Task associated with the put port of the thread decoupler
  virtual task put_encrypt(uvm_object arg);
    encrypt();
  endtask
  ...
endclass

Finally we need to connect this port implementation with the one from the uvm_thread_decoupler:

class cfs_enc_model extends uvm_component;
  ...
  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    ...      
    encrypt_decoupler.port_execute.connect(port_encrypt_decoupler);
  endfunction
  ...
endclass

Step #4: Substitute the Target Task Call With execute() Task

The execute() task from uvm_thread_decoupler is a task which consumes the same amount of time as our target task. Calling it instead of the target task has no effect on the overall functionality … except, of course, for the decoupling part:

class cfs_enc_model extends uvm_component implements cfs_enc_reset_handler;
  ...
  //Function called when the software started the encryption
  virtual function void sw_encryption_start(bit prev_value, bit next_value);
    fork
      begin
        //Replace the call to encrypt() with the call to execute()
        encrypt_decoupler.execute();
      end
    join_none
  endfunction
  ...
endclass

Step #5: Kill the encrypt() Task at the Correct Reset

We can still kill the encrypt() task at the correct reset via handle_reset() function of the uvm_thread_decoupler:

class cfs_enc_model extends uvm_component implements cfs_enc_reset_handler;
  ...
  virtual function void  handle_reset(uvm_phase phase, string kind);
    if(kind == "SYS") begin
      //Kill the encrypt() task
      encrypt_decoupler.handle_reset(phase, kind);
    end
    else if(kind == "HARD") begin
      ...
    end
    else begin
     `uvm_error("ERROR", $sformatf("Unknown reset kind: %0s", kind))
    end
  endfunction
  ...
endclass

That’s it. At this point the encrypt() task is decoupled from the APB reset and it will continue to run even if collect_transactions() task from APB monitor is killed at APB reset.

You can also play around with this code in a small project I build on EDA Playground.

Explanation: How uvm_thread_decoupler Works?

The way uvm_thread_decoupler decouples the execute() task from its caller process is by …. not executing anything 🙂 Whenever there is a call to execute() this … “executing” intent is saved into a queue. The actual executing is done from uvm_thread_decoupler run_phase(), which is in a different process than the caller of execute().

Bellow there is a simplified version of execute() task. The most important actions performed by it are:

  • register in pending_threads queue a request to execute the decoupled task (a.k.a. saving the executing intent)
  • emit thread_started event to inform the main loop from run_phase() that pending_threads has a new entry in it and it must handle it
class uvm_thread_decoupler extends uvm_component;
  ...
  //Task to execute the decoupled thread
  virtual task execute(uvm_object arg = null);
    ...
    //Put in a local queue information regarding this 
    pending_threads.push_back(info);

    //Inform the main loop from run_phase() that there is a new request
    ->thread_started;
    ...
  endtask
  ...
endtask

In the run_phase() task of uvm_thread_decoupler there is an endless loop which listens for any emit of thread_started and only then it calls the put() task of its TLM analysis port, which actually calls the decoupled task (see Step #3):

class uvm_thread_decoupler extends uvm_component;
  ...
  virtual task run_phase(uvm_phase phase);
    forever begin
      ...
      @(thread_started);

      //Get the information on the task to execute
      info = pending_threads.pop_front()

      //Call the actual task which needs to be decoupled
      port_execute.put(info.get_thread_arg());
      ...
    end
  endtask
  ...
endtask

So because run_phase() is in a different process than execute(), the call to port_execute.put() (which calls the actual decoupled task) is not affected by the process of execute().

Again, the two code snippets from above are just simplified versions of the actual implementation in order to highlight the idea behind uvm_thread_decoupler. The actual implementation just adds details on top of this idea.

If you take a closer look at the actual code from uvm_thread_decoupler.sv, the added complexity has several reasons:

  • add the ability to decouple tasks which require arguments when called
  • add the ability to handle multiple calls to the decoupled task in the same time
  • add the ability to kill one particular instance of the decoupled task
  • add the ability to kill all running decoupled tasks (e.g. at reset)

I also tried to verify the code of uvm_thread_decoupler using the SVUnit framework and it proved to be an extremely useful and quick method for ironing out some of the bugs.

You can play around with my attempt of using SVUnit in this EDA Playground project.

Hope you find this article useful 🙂


Related Articles

Cristian Slav

Leave a Reply

Your email address will not be published.