Interrupt Controller & System Events
Modeling an Interrupt Controller (GIC/NVIC) and bridging hardware IRQs into TLM software interrupts.
How to Read This Lesson
Interrupt Controllers & System Events
In a real System-on-Chip (SoC), peripherals do not expect the CPU to constantly poll them. When a Timer expires or a UART receives data, it asserts a hardware interrupt line (IRQ).
The Interrupt Controller (like the ARM GIC or Cortex-M NVIC) receives dozens of these raw hardware lines, prioritizes them, and signals the CPU. In a Virtual Platform, we must model this exact behavior.
Standard and source context
The Architecture
- Peripheral: Asserts a standard
sc_signal<bool>representing the IRQ line. - Interrupt Controller (INTC): Contains a TLM socket (for the CPU to read status/acknowledge) and standard
sc_in<bool>ports for the incoming IRQ lines. It evaluates priority and asserts a singlesc_out<bool>to the CPU. - CPU ISS: A thread monitoring the
sc_in<bool>from the INTC, triggering an asynchronous exception routine in the simulated software.
Complete Interrupt Controller Example
Here is a complete sc_main demonstrates a peripheral generating an interrupt, the controller routing it, and the CPU responding.
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
// 1. Mock Peripheral (Timer that fires an IRQ)
SC_MODULE(Timer_IRQ) {
sc_core::sc_out<bool> irq_out{"irq_out"};
SC_CTOR(Timer_IRQ) {
SC_THREAD(run);
}
void run() {
wait(20, sc_core::SC_NS);
std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Firing IRQ." << std::endl;
irq_out.write(true); // Assert Interrupt
}
};
// 2. The Interrupt Controller
SC_MODULE(InterruptController) {
tlm_utils::simple_target_socket<InterruptController> socket;
sc_core::sc_in<bool> irq_in{"irq_in"};
sc_core::sc_out<bool> cpu_irq{"cpu_irq"};
bool irq_pending = false;
SC_CTOR(InterruptController) : socket("socket") {
socket.register_b_transport(this, &InterruptController::b_transport);
SC_METHOD(eval_interrupts);
sensitive << irq_in;
}
private:
void eval_interrupts() {
if (irq_in.read() == true) {
std::cout << "@" << sc_core::sc_time_stamp() << " [INTC] IRQ Received. Forwarding to CPU." << std::endl;
irq_pending = true;
cpu_irq.write(true);
}
}
void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
// Mocking the CPU acknowledging and clearing the interrupt
if (trans.get_command() == tlm::TLM_WRITE_COMMAND && trans.get_address() == 0x10) { // 0x10 = Clear Reg
std::cout << "@" << sc_core::sc_time_stamp() << " [INTC] CPU Cleared IRQ via TLM." << std::endl;
irq_pending = false;
cpu_irq.write(false);
}
trans.set_response_status(tlm::TLM_OK_RESPONSE);
}
};
// 3. Mock CPU
SC_MODULE(MockCPU_IRQ) {
tlm_utils::simple_initiator_socket<MockCPU_IRQ> socket;
sc_core::sc_in<bool> irq_in{"irq_in"};
SC_CTOR(MockCPU_IRQ) : socket("socket") {
SC_THREAD(cpu_loop);
// Under the hood: This maps to the sc_signal::m_posedge_event
sensitive << irq_in.pos();
}
void cpu_loop() {
while(true) {
wait(); // Wait for IRQ
std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] INTERRUPT DETECTED! Jumping to ISR." << std::endl;
// Send TLM transaction to INTC to clear the interrupt
tlm::tlm_generic_payload trans;
sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
uint32_t val = 1;
trans.set_command(tlm::TLM_WRITE_COMMAND);
trans.set_address(0x10);
trans.set_data_ptr(reinterpret_cast<unsigned char*>(&val));
trans.set_data_length(4);
trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
socket->b_transport(trans, delay);
wait(delay); // Advance time for bus latency
}
}
};
int sc_main(int argc, char* argv[]) {
// Hardware wires
sc_core::sc_signal<bool> timer_irq_wire;
sc_core::sc_signal<bool> cpu_irq_wire;
// Instantiate Modules
Timer_IRQ timer("timer");
InterruptController intc("intc");
MockCPU_IRQ cpu("cpu");
// Bind Wires
timer.irq_out(timer_irq_wire);
intc.irq_in(timer_irq_wire);
intc.cpu_irq(cpu_irq_wire);
cpu.irq_in(cpu_irq_wire);
// Bind TLM Socket
cpu.socket.bind(intc.socket);
sc_core::sc_start(100, sc_core::SC_NS);
return 0;
}LT Temporal Decoupling vs Hardware Interrupts
There is a severe synchronization issue when mixing TLM Loosely Timed (LT) initiators with discrete hardware events.
The Problem: In an LT CPU using a tlm_quantumkeeper, the CPU is accumulating local_time natively within a for() loop, running ahead of global sc_time_stamp(). If the CPU is currently at local time 40 ns (but the global scheduler is at 0 ns), and a timer fires a physical IRQ at global time 20 ns, the CPU has technically "overshot" the interrupt! The software state has already executed instructions past the point where it should have been preempted by the ISR.
The Solution: Production CPU wrappers (like open-source RISC-V ISS models or QEMU-SystemC bridges) must implement a quantum-break mechanism. When irq_in.pos() is triggered inside the hardware domain, the CPU wrapper must immediately assert a flag (m_async_irq_pending). The LT execution loop must check this flag after every instruction. If asserted, it forces an immediate wait(local_time) to sync back with the sc_simcontext, processes the ISR, and resets the quantum.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections