Chapter 12: Virtual Platform Construction

The CPU Wrapper (ISS)

Wrapping a simple instruction sequence to act as a bus master, and assembling the final Virtual Platform.

Building a Virtual Platform: The CPU Wrapper

In a production Virtual Platform (VP), the CPU is usually an Instruction Set Simulator (ISS) provided by a vendor like ARM (FastModels) or Imperas. The ISS decodes cross-compiled .elf binaries. Whenever the executing software performs a Load (LDR) or Store (STR) to physical memory, the ISS generates a TLM transaction and pushes it out its initiator socket.

For our VP, we will build a "Dummy CPU" acting as the bus master. It executes a hardcoded sequence of TLM reads and writes to configure the Timer and test the RAM.

The Complete Assembled Virtual Platform

This file acts as the culmination of the Virtual Platform chapter. It contains the Mock CPU, the Router (from Chapter 12.2), the RAM, the Timer (from 12.3), and the sc_main that binds them together.

This model perfectly conforms to the Accellera Simple Bus AT/LT specifications.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <vector>
 
// --- Targets (Peripherals) ---
class RAM : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<RAM> socket;
    unsigned char* memory;
 
    RAM(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        memory = new unsigned char[0x40000]; // 256 KB
        socket.register_b_transport(this, &RAM::b_transport);
    }
    ~RAM() { delete[] memory; }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            memcpy(&memory[trans.get_address()], trans.get_data_ptr(), trans.get_data_length());
        }
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
        delay += sc_core::sc_time(10, sc_core::SC_NS);
    }
};
 
class Timer : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<Timer> socket;
    unsigned int counter = 0;
 
    Timer(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        socket.register_b_transport(this, &Timer::b_transport);
    }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        if (trans.get_command() == tlm::TLM_WRITE_COMMAND && trans.get_address() == 0x0) {
            counter = 100; // Mock setting the timer
        }
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};
 
// --- Router (Interconnect) ---
class Router : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<Router> target_socket;
    tlm_utils::simple_initiator_socket<Router> init_ram;
    tlm_utils::simple_initiator_socket<Router> init_timer;
 
    Router(sc_core::sc_module_name name) : sc_core::sc_module(name), target_socket("target_socket"), init_ram("init_ram"), init_timer("init_timer") {
        target_socket.register_b_transport(this, &Router::b_transport);
    }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 addr = trans.get_address();
        
        if (addr >= 0x00000000 && addr <= 0x0003FFFF) {
            init_ram->b_transport(trans, delay);
        } else if (addr >= 0x40000000 && addr <= 0x40000FFF) {
            trans.set_address(addr - 0x40000000);
            init_timer->b_transport(trans, delay);
            trans.set_address(addr); // Restore!
        } else {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }
    }
};
 
// --- Initiator (Mock CPU) ---
class CPU : public sc_core::sc_module {
public:
    tlm_utils::simple_initiator_socket<CPU> socket;
 
    SC_HAS_PROCESS(CPU);
    CPU(sc_core::sc_module_name name) : sc_core::sc_module(name), socket("socket") {
        SC_THREAD(execute_firmware);
    }
 
private:
    void execute_firmware() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        unsigned int data;
 
        std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] Booting..." << std::endl;
 
        // 1. Write to Timer Control (Address 0x40000000)
        data = 1;
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x40000000);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
        
        socket->b_transport(trans, delay);
        wait(delay); 
 
        // 2. Write to RAM (Address 0x00000100)
        delay = sc_core::SC_ZERO_TIME;
        data = 0xDEADBEEF;
        trans.set_address(0x00000100);
        trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
        
        socket->b_transport(trans, delay);
        wait(delay);
 
        std::cout << "@" << sc_core::sc_time_stamp() << " [CPU] Firmware execution halted." << std::endl;
    }
};
 
// --- Top Level Assembly ---
int sc_main(int argc, char* argv[]) {
    CPU cpu("cpu");
    Router router("router");
    RAM ram("ram");
    Timer timer("timer");
 
    // Bind CPU -> Router
    cpu.socket.bind(router.target_socket);
 
    // Bind Router -> Peripherals
    router.init_ram.bind(ram.socket);
    router.init_timer.bind(timer.socket);
 
    std::cout << "Starting Virtual Platform Simulation..." << std::endl;
    sc_core::sc_start();
 
    return 0;
}

Conclusion

Congratulations! You have built a fully functional Electronic System Level (ESL) Virtual Platform.

You constructed standard targets (RAM, Timer), a Doulos-compliant routing interconnect, and an initiator that acts as the bus master. Silicon companies use variations of this exact architecture to develop, test, and boot massive operating systems (like Linux or Android) months or years before the physical silicon is ever manufactured in a fab.

Comments and Corrections