How to Decouple Threads in SystemVerilog
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.
To makes things interesting, our DUT has the registers in the APB reset domain, and its internal encryption engine in the system reset domain.
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.
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():
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
- How to Use Register Callbacks in uvm_reg Library
- SystemVerilog: How To Handle Reset In UVM (part 1)
- Multiple Inheritance In SystemVerilog
