Chapter 12: Virtual Platform Construction

Interrupt Controller & System Events

Modeling an Interrupt Controller (GIC/NVIC) and bridging hardware IRQs into TLM software interrupts.

Listen to this lessonAudiobook mode

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

  1. Peripheral: Asserts a standard sc_signal<bool> representing the IRQ line.
  2. 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 single sc_out<bool> to the CPU.
  3. 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.

Lesson self-check

Can you answer these clearly?

Keep moving when you can answer each question without looking back at the lesson.

Comments and Corrections