How to handle interrupt in UVM?
February 7, 2022 2022-02-07 18:23How to handle interrupt in UVM?
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.