How to handle Interrupt in UVM?

Interrupt handling is a well-known feature of any SoC which usually comprises of CPU, Bus Fabric, several Controllers, Sub-Systems & many IP blocks as part of it. In some way or other Interrupts are used to act as the sideband signals of the Design/IP Blocks & most of the time it’s not the part of the main bus or control bus.

Fundamentally, Interrupts are the events that trigger a new thread of processing. Usually Interrupt acts in a System or Sub-System environment, where Design or IP blocks generate an event once certain design conditions are met or fulfilled. These events might be expected to propagate to the CPU via Interrupt Controller. An Interrupt Controller helps to manage several interrupts from different IPs, prioritize or arbitrate these interrupts. Interrupt Controller can be configured to enable & disable interrupts & can accept multiple interrupt request lines.

Coming back to the role of Interrupts in a System – it triggers a new thread of processing. This new thread which is usually called the Interrupt Service Routine (ISR) can either take the place of the current execution thread, or it can be used to wake up a sleeping process to initiate some hardware activity.

Now, let’s see how using UVM the Interrupt Service Routine is serviced when an interrupt is asserted. The simplest way to model interrupt handling is to trigger the execution of a Sequence that uses the grab() method to get exclusive access to the Sequencer. In this way, the current stimulus generation is disrupted but this is actually what happens when an ISR is triggered on a CPU. The interrupt service routine that is represented in the form of a Sequence can not be interrupted itself, & must make an ungrab() call before it completes.

Now let’s see it through an example of UVM code below:

// Top-Level Sequence
class top_level_seq extends uvm_sequence #(transaction);
 `uvm_object_utils(top_level_seq)
 
 function new (string name);
 super.new(name);
 endfunction: new
 
 task body;
  // Sequence instantiation
 main_seq MAIN_SEQ;
 isr ISR;
 
 // Interrupt specific configuration class
 int_config INT_CONF;
 
 MAIN_SEQ = main_seq::type_id::create("MAIN_SEQ", this);
 ISR = isr::type_id::create("ISR", this);
 if (!uvm_config_db #(int_config)::get(null, get_full_name(), "int_confir", INT_CONF)) begin
 `uvm_error("TOP SEQ BODY", "Failed to get int_config");
 end
 
 // Two level of forked process
 fork
   PRI_SEQ.start(m_sequencer);
    begin
     forever begin
      fork
        INT_CONF.wait_for_IRQ0();
        INT_CONF.wait_for_IRQ1();
        INT_CONF.wait_for_IRQ2();
        INT_CONF.wait_for_IRQ3();
      join_any
      disable fork;
      ISR.start(m_sequencer)
    end
  end
 join_any
 disable fork;
endtask: body
 
endclass: top_level_seq

This is the top-level sequence i.e. top_level_seq which controls both main Sequences i.e. main_seq and Interrupt Service Routine (ISR) Sequence i.e. isr. In the main sequence a configuration class i.e. int_config is instantiated that contains the hardware synchronization tasks for the interrupts i.e. wait_for_IRQx().

The most important piece of the main sequence is the two-level forked process. In the first level, primarily two processes are spawned – The main sequence & the Interrupt assertion on any one of the Interrupts out of four possibilities i.e. IRQ1-IRQ4. The second level of the fork process is encapsulated in a forever loop. Once an Interrupt is sensed, other second-level processes (IRQx) are disabled using disable fork and active Interrupt is serviced & finally due to forever loop, all the 4-second levels of processes are spawned again to poll the interrupts.

Once the main sequence is over, there is no point of keep running and waiting for the interrupts, hence the fork..join_any process is disabled using disable fork at the first level.

Now let’s examine the code for 2 other Sub-Sequences below:

// Main Sequence
class main_seq extends uvm_sequence #(transaction);
 `uvm_object_utils(main_seq)
  
 function new (string name);
  super.new(name);
 endfunction: new
 
 task body;
 
 transaction req;
  req = transaction::type_id::create("transaction", this);
 
 repeat(150) begin
 start_item(req);
 if (!req.randomize() with {addr inside {[32'h0010_0000:32'h0010_001C]}; read_not_write == 0;}) begin
 `uvm_error("MAIN SEQ BODY", "req randomization failure")
 end
 finish_item();
 end
 endtask: body
 
endclass: main_seq

// ISR Sequence
class isr extends uvm_sequence #(transaction);
 `uvm_object_utils(isr)
 
 function new (string name);
  super.new(name);
 endfunction: new
 
 // Request data
 rand logic [31:0] addr;
 rand logic [31:0] write_data;
 rand bit read_not_write;
 rand int delay;
 
 // Response data
 bit error;
 logic [31:0] read_data
 
 task body;
 transaction req;
 
 // Grabbing the sequencer
 m_sequencer.grab(this);
 
 req = transaction::type_id::create("transaction", this);
 
 //Read from the status register to determine the cause of interrupt
 if(!req.randomize() with {addr == 32'0010_0000; read_not_write == 1;}) begin
 `uvm_error("INT SEQ BODY", "randomization failure")
 end
 start_item(req);
 finish_item(req);
 
 // Clear the IRQ bit
 req.read_not_write = 0;
 if(req.read_data[0] == 1)
 begin
 `uvm_info("ISR SEQ BODY", "IRQ[0] detected", UVM_LOW)
 req.write_data[0] = 0;
 start_item(req);
 finish_item(req);
 `uvm_info("ISR SEQ BODY", "IRQ[0] cleared", UVM_LOW)
 end
 
 if(req.read_data[1] == 1)
 begin
 `uvm_info("ISR SEQ BODY", "IRQ[1] detected", UVM_LOW)
 req.write_data[1] = 0;
 start_item(req);
 finish_item(req);
 `uvm_info("ISR SEQ BODY", "IRQ[1] cleared", UVM_LOW)
 end
 
 if(req.read_data[2] == 1)
 begin
 `uvm_info("ISR SEQ BODY", "IRQ[2] detected", UVM_LOW)
 req.write_data[2] = 0;
 start_item(req);
 finish_item(req);
 `uvm_info("ISR SEQ BODY", "IRQ[2] cleared", UVM_LOW)
 end
 
 if(req.read_data[3] == 1)
 begin
 `uvm_info("ISR SEQ BODY", "IRQ[3] detected", UVM_LOW)
 req.write_data[3] = 0;
 start_item(req);
 finish_item(req);
 `uvm_info("ISR SEQ BODY", "IRQ[3] cleared", UVM_LOW)
 end

 // Processing the transaction with interrupt line low
 start_item(req);
 finish_item(req);
 
 // Releasing the Sequencer for the main Sequence
 m_sequencer.ungrab(this);
 
 endtask: body
 
endclass: isr

In the above UVM code, the main sequence i.e. main_seq is pretty straightforward. It is implementing a loop (150 times) in which the transaction i.e. req is sent to the UVM Driver & finally to the bus interface for that many times.

The important thing to observe in the Interrupt Service Routine (ISR) Sequence i.e. isr is the use of grab() and ungrab() tasks. Another important thing to notice is the interrupt priority structure. The written order is important and the top entry in IRQ[1]-IRQ[4] i.e. IRQ[1] is serviced first. Other key functional information is provided in the form of comments along with the code, please refer to that. Using the grab(), isr Sequence takes full control of the Sequencer and performs the defined tasks of the ISR, and later once done using ungrab() call, it releases the Sequencer for the Main Sequence to continue.

I hope you enjoyed the learning. Please, feel free to put your comments and/or suggestions for future topics. This blog post is amazingly explained by one of the senior-most engineer from Mentor Graphics Manish. Till then see ya take care and Keep on learning Keep on Growing 🙂

About the author

The Art of Verification

Hi, I’m Hardik, and welcome to The Art of Verification.

I’m a Verification Engineer who loves to crack complex designs and here to help others commit to mastering Verification Skills through self-learning, System Verilog, UVM, and most important to develop that thought process that every verification engineer should have.

I’ve made it my mission to give back and serve others beyond myself.

I will NEVER settle for less than I can be, do, give, or create.

View all posts