Chapter 12: Virtual Platform Construction

VP Firmware Traffic and Real-World Flow

A complete TLM-2.0 Loosely Timed (LT) VP scenario with firmware traffic, routing, memory access, and timing.

How to Read This Lesson

For virtual platforms, imagine a firmware engineer trying to boot real software on your model. Every abstraction choice should help that person move faster without lying about the hardware.

VP Firmware Traffic and Real-World Flow

The final Virtual Platform (VP) should tell a story that accurately represents real firmware bring-up. In an industrial setting, a VP connects standard bus architectures, executes firmware instructions from a CPU initiator, routes transactions through an interconnect, and manipulates target peripherals.

Source and LRM Trail

Virtual platform lessons combine standard TLM behavior with architecture practice. Use Docs/LRMs/SystemC_LRM_1666-2023.pdf for TLM and kernel rules, .codex-src/systemc/src/tlm_core/tlm_2 for sockets and payloads, .codex-src/cci for configurable platforms, and .codex-src/systemc-common-practices for reusable patterns.

Boot Sequence

Even with a dummy CPU initiator, we can simulate a standard firmware-like sequence:

  1. Write a configuration byte to a peripheral.
  2. Read a status register from memory.
  3. Advance simulation time correctly based on memory latency.

This demonstrates interconnect routing, target response handling, and Loosely Timed (LT) quantum accumulation.

Standard Doulos / Accellera Interconnect Pattern

To demonstrate this cleanly, we abandon proprietary wrappers and rely exclusively on the IEEE 1666 standard TLM-2.0 b_transport interface and a standard Simple Bus / Interconnect model.

In this architecture:

  • An Initiator generates payload transactions.
  • A Router (Interconnect) inspects the payload's address and forwards it to the correct target.
  • The Targets (Memory, Peripherals) implement the b_transport interface, apply latency via the sc_time reference parameter, and return standard TLM_OK_RESPONSE statuses.

Complete End-to-End Example

The following is a 100% complete, compilable SystemC model of a Loosely Timed VP running a firmware boot traffic scenario over a standard interconnect.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
 
using namespace sc_core;
using namespace tlm;
 
// ---------------------------------------------------------
// Target 1: Memory (Base Address: 0x0000)
// ---------------------------------------------------------
class MemoryTarget : public sc_module {
public:
    tlm_utils::simple_target_socket<MemoryTarget> socket;
    unsigned char mem[1024];
 
    SC_HAS_PROCESS(MemoryTarget);
    MemoryTarget(sc_module_name name) : sc_module(name), socket("socket") {
        socket.register_b_transport(this, &MemoryTarget::b_transport);
        for(int i=0; i<1024; ++i) mem[i] = 0; // Initialize memory
        mem[0x10] = 0xAA; // Pre-load a "Boot Status" value
    }
 
    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        tlm_command cmd = trans.get_command();
        sc_dt::uint64 addr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();
        unsigned int len = trans.get_data_length();
 
        // Memory target only handles offsets 0x0000 - 0x03FF
        if (addr >= 1024) {
            trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }
 
        if (cmd == TLM_READ_COMMAND) {
            memcpy(ptr, &mem[addr], len);
        } else if (cmd == TLM_WRITE_COMMAND) {
            memcpy(&mem[addr], ptr, len);
        }
 
        // Apply a realistic memory access latency
        delay += sc_time(20, SC_NS);
        trans.set_response_status(TLM_OK_RESPONSE);
    }
};
 
// ---------------------------------------------------------
// Target 2: Peripheral (Base Address: 0x1000)
// ---------------------------------------------------------
class UARTPeripheral : public sc_module {
public:
    tlm_utils::simple_target_socket<UARTPeripheral> socket;
 
    SC_HAS_PROCESS(UARTPeripheral);
    UARTPeripheral(sc_module_name name) : sc_module(name), socket("socket") {
        socket.register_b_transport(this, &UARTPeripheral::b_transport);
    }
 
    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        tlm_command cmd = trans.get_command();
        unsigned char* ptr = trans.get_data_ptr();
 
        if (cmd == TLM_WRITE_COMMAND) {
            // Firmware writing to UART TX register
            std::cout << "[UART] Transmitting byte: 0x" << std::hex << (int)(*ptr) << std::dec << "\n";
        }
        
        // Peripheral access is slower than memory
        delay += sc_time(100, SC_NS); 
        trans.set_response_status(TLM_OK_RESPONSE);
    }
};
 
// ---------------------------------------------------------
// Interconnect: Simple Router
// ---------------------------------------------------------
class Interconnect : public sc_module {
public:
    tlm_utils::simple_target_socket<Interconnect> target_socket;
    tlm_utils::simple_initiator_socket<Interconnect> init_socket_mem;
    tlm_utils::simple_initiator_socket<Interconnect> init_socket_uart;
 
    SC_HAS_PROCESS(Interconnect);
    Interconnect(sc_module_name name) : sc_module(name) {
        target_socket.register_b_transport(this, &Interconnect::b_transport);
    }
 
    void b_transport(tlm_generic_payload& trans, sc_time& delay) {
        sc_dt::uint64 addr = trans.get_address();
 
        // Standard address decoding map
        if (addr < 0x1000) {
            // Route to Memory
            init_socket_mem->b_transport(trans, delay);
        } else if (addr >= 0x1000 && addr < 0x2000) {
            // Route to UART and subtract base address for local offset
            trans.set_address(addr - 0x1000);
            init_socket_uart->b_transport(trans, delay);
            // Restore original address to maintain generic payload contract
            trans.set_address(addr);
        } else {
            trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
        }
    }
};
 
// ---------------------------------------------------------
// Initiator: Firmware CPU Wrapper
// ---------------------------------------------------------
class FirmwareCPU : public sc_module {
public:
    tlm_utils::simple_initiator_socket<FirmwareCPU> socket;
 
    SC_HAS_PROCESS(FirmwareCPU);
    FirmwareCPU(sc_module_name name) : sc_module(name), socket("socket") {
        SC_THREAD(execute_firmware);
    }
 
    void execute_firmware() {
        tlm_generic_payload trans;
        sc_time delay = SC_ZERO_TIME;
        unsigned char data;
 
        std::cout << "Time " << sc_time_stamp() << ": Firmware booting...\n";
 
        // 1. Read Boot Status from Memory (Address 0x0010)
        trans.set_command(TLM_READ_COMMAND);
        trans.set_address(0x0010);
        trans.set_data_ptr(&data);
        trans.set_data_length(1);
        trans.set_response_status(TLM_INCOMPLETE_RESPONSE);
 
        socket->b_transport(trans, delay);
        
        if (trans.get_response_status() == TLM_OK_RESPONSE) {
            std::cout << "Time " << sc_time_stamp() << ": Read boot status: 0x" << std::hex << (int)data << std::dec << " (Accumulated Delay: " << delay << ")\n";
        }
 
        // 2. Consume accumulated delay to sync with SystemC kernel
        wait(delay); 
        delay = SC_ZERO_TIME; 
 
        // 3. Write 'O' (0x4F) to UART (Address 0x1000)
        data = 0x4F; 
        trans.set_command(TLM_WRITE_COMMAND);
        trans.set_address(0x1000);
        trans.set_response_status(TLM_INCOMPLETE_RESPONSE);
 
        socket->b_transport(trans, delay);
        wait(delay); // Sync again
 
        std::cout << "Time " << sc_time_stamp() << ": Firmware execution complete.\n";
    }
};
 
// ---------------------------------------------------------
// Top Level
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    FirmwareCPU cpu("cpu");
    Interconnect bus("bus");
    MemoryTarget mem("mem");
    UARTPeripheral uart("uart");
 
    // Bindings
    cpu.socket.bind(bus.target_socket);
    bus.init_socket_mem.bind(mem.socket);
    bus.init_socket_uart.bind(uart.socket);
 
    sc_start();
    return 0;
}

What This Architecture Demonstrates

A professional VP architect ensures the code communicates real hardware intent:

  • Address Decoding: The Interconnect model acts as a router, stripping base addresses to provide the target with a 0-indexed local offset, matching real-world IP block behavior.
  • Timing Accumulation: In Loosely Timed (LT) models, the initiator (FirmwareCPU) accumulates time in the delay variable during b_transport calls, but the SystemC kernel time sc_time_stamp() does not advance until wait(delay) is explicitly called. This enables blazing-fast simulation without sacrificing causality.
  • Payload Contracts: Initiators must reset the response status to TLM_INCOMPLETE_RESPONSE before sending, and targets must set it to TLM_OK_RESPONSE (or an error) before returning.
  • Doulos / Standard Guidelines: The use of tlm_utils::simple_target_socket cleanly encapsulates interface implementation boilerplate, matching the standard Accellera LT examples.

Under the Hood: simple_target_socket C++ Implementation

When you use tlm_utils::simple_target_socket, you are bypassing the need to manually inherit from and implement the tlm::tlm_fw_transport_if.

In the official Accellera repository, simple_target_socket inherits from tlm::tlm_target_socket<BUSWIDTH, TYPES>. It encapsulates an internal nested class called fw_process that actively implements b_transport, nb_transport_fw, get_direct_mem_ptr, and transport_dbg.

When you call socket.register_b_transport(this, &MemoryTarget::b_transport), the socket stores an sc_core::sc_spawn_options and a functor (or member function pointer adapter) inside its internal state.

During simulation, when the initiator calls init_socket->b_transport(trans, delay), it traverses the bound SystemC port array and lands directly on the target's fw_process::b_transport. This internal method checks if a custom b_transport callback was registered. If yes, it dereferences the functor and executes your MemoryTarget::b_transport directly in the execution context of the initiator's thread. This abstraction provides a massive productivity boost while compiling down to zero-overhead C++ virtual function calls.

Comments and Corrections