Chapter 12: Virtual Platform Construction

Interrupt Controller & System Events

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

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.

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

This 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);
        sensitive << irq_in.pos(); // Wake up asynchronously on interrupt
    }
 
    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;
}

This structural pattern correctly synchronizes continuous hardware events with the loosely timed TLM environment, ensuring interrupt latency and causality are accurately preserved.

Comments and Corrections