Chapter 12: Virtual Platform Construction

VP Architecture Review for Technical Leads

A lead-engineer checklist and compliant architectural example for reviewing a SystemC virtual platform before deployment.

Listen to this lessonAudiobook mode

How to Read This Lesson

VP Architecture Review for Technical Leads

This page is written for the technical lead who must decide whether a Virtual Platform (VP) is ready for firmware bring-up, architecture exploration, or customer release. A VP is only useful if it acts as a reliable, unambiguous contract between hardware designers and software engineers.

Standard and source context

The Review Checklist

Before a VP is deployed, it must be evaluated against the following criteria:

1. Memory Map Contract

Check that every region has a strictly defined:

  • Base address and size (preventing overlaps and undefined holes).
  • Access width policy and Endianness.
  • Error response policy (what happens on unmapped accesses?).
  • Debug transport support (transport_dbg).

2. Register Quality

If a firmware engineer cannot write a driver from the documentation and the model's behavior, the VP is incomplete. Every peripheral must enforce:

  • Reset values and reserved bits.
  • Read-only vs Write-only semantics.
  • Side effects (e.g., clear-on-read).
  • Interrupt generation policies.

3. Timing Contract

The VP must state exactly what simulation time means. Examples:

  • "RAM access latency is modeled as a constant 20ns TLM delay."
  • "UART transmission is one character delay per byte."
  • "Interrupt propagation uses SystemC sc_signal update semantics."

4. Configuration and Observability

  • CCI parameters must be named stably, documented, and properly locked if structural.
  • The VP must detect unconsumed presets.
  • Use standardized SystemC reporting macros (SC_REPORT_INFO, SC_REPORT_FATAL) instead of raw std::cout.

Complete Example: The "Review-Ready" Compliant Block

The following complete, compilable example demonstrates a peripheral that strictly adheres to the review criteria above. It models a compliant timer peripheral with strict register semantics, debug transport, CCI configuration, proper memory bounds checking, and explicitly stated timing contracts.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <cci_configuration>
 
using namespace sc_core;
using namespace tlm;
 
class CompliantTimer : public sc_module {
public:
    // Memory map socket
    tlm_utils::simple_target_socket<CompliantTimer> socket;
    
    // Interrupt out
    sc_out<bool> irq_out;
 
    // CCI Configuration (Review #4)
    cci::cci_param<int> base_frequency_hz;
 
    SC_HAS_PROCESS(CompliantTimer);
    CompliantTimer(sc_module_name name) 
        : sc_module(name)
        , socket("socket")
        , irq_out("irq_out")
        , base_frequency_hz("base_frequency_hz", 1000, "Base clock frequency in Hz")
    {
        socket.register_b_transport(this, &CompliantTimer::b_transport);
        socket.register_transport_dbg(this, &CompliantTimer::transport_dbg);
        
        SC_THREAD(timer_process);
    }
 
private:
    // Registers (Review #2: Register Quality)
    // 0x00: CTRL (Bit 0: Enable, Bit 1: Interrupt Enable)
    // 0x04: STATUS (Bit 0: Timer Fired - Clear on Write 1)
    uint32_t reg_ctrl = 0;
    uint32_t reg_status = 0;
 
    sc_event ev_timer_fired;
 
    // Review #3: Timing Contract.
    // Register access is modeled as a fixed 10ns delay.
    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();
 
        // Review #1: Memory Map Contract (Bounds and access width checking)
        if (addr > 0x04 || len != 4) {
            trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }
 
        if (cmd == TLM_READ_COMMAND) {
            uint32_t val = (addr == 0x00) ? reg_ctrl : reg_status;
            memcpy(ptr, &val, 4);
        } else if (cmd == TLM_WRITE_COMMAND) {
            uint32_t val;
            memcpy(&val, ptr, 4);
            
            if (addr == 0x00) {
                reg_ctrl = val & 0x03; // Mask reserved bits
                SC_REPORT_INFO("Timer", "CTRL register updated.");
            } else if (addr == 0x04) {
                // Clear on write 1
                if (val & 0x01) {
                    reg_status &= ~0x01;
                    irq_out.write(false);
                }
            }
        }
 
        delay += sc_time(10, SC_NS); // Apply timing contract
        trans.set_response_status(TLM_OK_RESPONSE);
    }
 
    // Review #1 & #6: Debug transport bypasses delays and side-effects
    unsigned int transport_dbg(tlm_generic_payload& trans) {
        sc_dt::uint64 addr = trans.get_address();
        if (addr > 0x04 || trans.get_data_length() != 4) return 0;
        
        uint32_t val = (addr == 0x00) ? reg_ctrl : reg_status;
        memcpy(trans.get_data_ptr(), &val, 4);
        return 4; // Bytes read
    }
 
    void timer_process() {
        while (true) {
            // Wait for 1 tick based on CCI parameter
            sc_time tick_period(1.0 / base_frequency_hz.get_value(), SC_SEC);
            wait(tick_period);
 
            if (reg_ctrl & 0x01) { // If Enabled
                reg_status |= 0x01; // Set status
                
                if (reg_ctrl & 0x02) { // If Interrupt Enabled
                    irq_out.write(true);
                }
            }
        }
    }
};
 
// --- Top Level Testbench ---
int sc_main(int argc, char* argv[]) {
    // Setup Broker (Review #4)
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
 
    // Instantiate and bind
    CompliantTimer timer("timer");
    sc_signal<bool> sig_irq;
    timer.irq_out(sig_irq);
 
    // Dummy transaction to test the contract
    tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    uint32_t data = 0x03; // Enable + IRQ Enable
 
    trans.set_command(TLM_WRITE_COMMAND);
    trans.set_address(0x00);
    trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
    trans.set_data_length(4);
    trans.set_response_status(TLM_INCOMPLETE_RESPONSE);
 
    // Send transaction (Simulating a CPU)
    timer.socket->b_transport(trans, delay);
    wait(delay); // Accumulate time
    
    if (trans.get_response_status() == TLM_OK_RESPONSE) {
        SC_REPORT_INFO("CPU", "Successfully configured timer.");
    }
 
    sc_start(2, SC_MS); // Run to see IRQ fire
    return 0;
}

The Approval Bar

A Virtual Platform is considered "lead-review ready" only when:

  • Software-visible behaviors (registers, interrupts) perfectly match documentation.
  • Extraneous configuration parameters are locked.
  • The boundary between Loosely Timed (LT) approximations and Cycle Accurate (CA) behavior is formally recorded.
  • Debug inspection (transport_dbg) operates independently from time-advancing data paths.

Under the Hood: transport_dbg and sc_signal Semantics

When reviewing a VP architecture, two C++ implementation details frequently cause subtle simulation bugs: transport_dbg violations and interrupt signal semantics.

1. The transport_dbg Contract The tlm_fw_transport_if explicitly separates b_transport from transport_dbg. Under the hood, transport_dbg is a purely synchronous, non-blocking C++ function call that lacks an sc_time argument. It is illegal to call sc_core::wait() or modify the target's internal state (e.g., clearing a FIFO or a "clear-on-read" register bit) inside transport_dbg. Backdoor tools like GDB debuggers invoke transport_dbg directly through the interconnect. If it modifies state, the debugger observing memory will permanently corrupt the simulation timeline.

2. Interrupt Propagation via sc_signal In the compliant example above, irq_out.write(true) is used. In the SystemC kernel, sc_signal::write() does not immediately change the signal's value. Instead, it schedules an update() request in the simulation kernel's event queue. The new value is applied during the Update Phase at the end of the current delta cycle. If the CPU initiator models its interrupt polling within a single b_transport quantum without yielding back to the SystemC scheduler via wait(), it will completely miss the interrupt. VPs must explicitly state whether they use TLM payload interrupts (immediate execution) or sc_signal pins (requires a delta cycle yield).

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