Chapter 6: Practice

Virtual Platform Patterns

How to combine CPUs, memories, buses, peripherals, interrupts, and firmware-visible behavior.

A virtual platform is a SystemC model of a system that software can run against. It usually trades pin-level detail for speed and architectural usefulness.

Industry Standard Architectures

When building Virtual Platforms in SystemC, avoid proprietary or vendor-specific bus models unless strictly necessary. Instead, strictly model the architecture after the official Accellera TLM-2.0 loosely-timed (LT) and approximately-timed (AT) open-source examples, or the industry-standard Doulos Simple Bus.

Most standards-compliant platforms contain:

  • CPU or instruction-set simulator wrapper (Initiator)
  • memory map and address decoder (Interconnect/Bus)
  • RAM and ROM models (Targets)
  • timers, interrupt controllers, UARTs, DMA (Targets & Initiators)
  • debug and tracing utilities

Address Decoding & The Simple Bus

An interconnect routes transactions by address. To demonstrate this pattern, here is a complete, compilable, and standards-compliant TLM-2.0 Simple Router model. It uses the simple_target_socket to receive transactions from an initiator (e.g., a CPU) and forwards them to the correct target (e.g., Memory or UART) via an array of simple_initiator_sockets.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <vector>
 
using namespace sc_core;
 
// 1. The Targets
SC_MODULE(MemoryBlock) {
  tlm_utils::simple_target_socket<MemoryBlock> socket{"socket"};
  SC_CTOR(MemoryBlock) { socket.register_b_transport(this, &MemoryBlock::b_transport); }
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    delay += sc_time(10, SC_NS);
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }
};
 
SC_MODULE(UartBlock) {
  tlm_utils::simple_target_socket<UartBlock> socket{"socket"};
  SC_CTOR(UartBlock) { socket.register_b_transport(this, &UartBlock::b_transport); }
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    delay += sc_time(50, SC_NS);
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
    if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
      std::cout << "[UART] Char: " << (char)*(trans.get_data_ptr()) << "\n";
    }
  }
};
 
// 2. The Simple Bus / Interconnect
SC_MODULE(SimpleBus) {
  tlm_utils::simple_target_socket<SimpleBus> target_socket{"target_socket"};
  tlm_utils::simple_initiator_socket<SimpleBus> init_socket_mem{"init_socket_mem"};
  tlm_utils::simple_initiator_socket<SimpleBus> init_socket_uart{"init_socket_uart"};
 
  SC_CTOR(SimpleBus) {
    target_socket.register_b_transport(this, &SimpleBus::b_transport);
  }
 
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    uint64_t addr = trans.get_address();
    
    // Address Map: 
    // 0x0000 - 0x0FFF : Memory
    // 0x1000 - 0x1FFF : UART
    if (addr < 0x1000) {
      init_socket_mem->b_transport(trans, delay);
    } else if (addr < 0x2000) {
      // Localize address for target
      trans.set_address(addr - 0x1000); 
      init_socket_uart->b_transport(trans, delay);
      // Restore address
      trans.set_address(addr);
    } else {
      trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
    }
  }
};
 
// 3. The CPU (Initiator)
SC_MODULE(CpuModel) {
  tlm_utils::simple_initiator_socket<CpuModel> socket{"socket"};
  SC_CTOR(CpuModel) { SC_THREAD(run); }
  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    char data = 'A';
    
    // Write to UART
    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(0x1000);
    trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
    trans.set_data_length(1);
    
    socket->b_transport(trans, delay);
    wait(delay); // Synchronize with simulation time
  }
};
 
int sc_main(int argc, char* argv[]) {
  CpuModel cpu("cpu");
  SimpleBus bus("bus");
  MemoryBlock mem("mem");
  UartBlock uart("uart");
  
  // Binding
  cpu.socket.bind(bus.target_socket);
  bus.init_socket_mem.bind(mem.socket);
  bus.init_socket_uart.bind(uart.socket);
  
  sc_start();
  return 0;
}

The bus checks the address, forwards the payload, and preserves timing annotations. Modifying the generic payload address is allowed, but you must restore it before the transaction returns to the caller.

Timing Strategy

Choose timing deliberately:

  • Untimed: fastest, good for pure software enablement.
  • Loosely timed (LT): good for early performance and firmware work, relies on b_transport and quantum keeping (temporal decoupling).
  • Approximately timed (AT): useful when ordering and protocol phases (e.g. BEGIN_REQ, END_REQ) matter.
  • Cycle-accurate: slower, needed for detailed microarchitecture questions.

Interrupts and Register Models

Interrupts can be signals, events, TLM messages, or register-visible state depending on the platform style. Firmware should observe a coherent interrupt controller behavior: pending bits, enables, priorities, acknowledge paths, and side effects.

Register behavior is where many virtual platforms become valuable. Model reset values, read-only bits, write-one-to-clear bits, reserved fields, side effects, and timing when needed.

Comments and Corrections