Chapter 12: Virtual Platform Construction

The VP Peripherals

Building RAM and Hardware Timer memory-mapped peripherals using TLM 2.0 Simple Target Sockets.

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.

Building a Virtual Platform: The Peripherals

In the previous step, our Router forwarded transactions to specific sockets based on physical memory addresses. Now, we build the targets on the other side of those sockets: a Memory block (RAM) and a Hardware Timer.

Now let's look at how the Accellera TLM 2.0 core standardizes these patterns.

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.

Complete Peripheral Example

Here is a complete, runnable example demonstrates the exact TLM 2.0 implementations for a standard RAM array and a register-based Hardware Timer.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
 
// 1. A Simple TLM RAM Peripheral
class RAM_Peripheral : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<RAM_Peripheral> socket;
    
    SC_HAS_PROCESS(RAM_Peripheral);
    RAM_Peripheral(sc_core::sc_module_name name, unsigned int size_bytes) 
        : sc_core::sc_module(name), size(size_bytes) {
        
        memory = new unsigned char[size];
        memset(memory, 0, size);
        socket.register_b_transport(this, &RAM_Peripheral::b_transport);
    }
    ~RAM_Peripheral() { delete[] memory; }
 
private:
    unsigned char* memory;
    unsigned int size;
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        sc_dt::uint64    adr = trans.get_address();
        unsigned char*   ptr = trans.get_data_ptr();
        unsigned int     len = trans.get_data_length();
 
        // Check if the transaction exceeds the boundaries of this specific RAM
        if (adr + len > size) {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }
 
        if (cmd == tlm::TLM_READ_COMMAND) {
            memcpy(ptr, &memory[adr], len);
        } else if (cmd == tlm::TLM_WRITE_COMMAND) {
            memcpy(&memory[adr], ptr, len);
        }
 
        // Advance simulation time to model access latency
        delay += sc_core::sc_time(10, sc_core::SC_NS);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};
 
// 2. A Register-Based Hardware Timer Peripheral
class Timer_Peripheral : public sc_core::sc_module {
public:
    tlm_utils::simple_target_socket<Timer_Peripheral> socket;
 
    SC_HAS_PROCESS(Timer_Peripheral);
    Timer_Peripheral(sc_core::sc_module_name name) : sc_core::sc_module(name) {
        socket.register_b_transport(this, &Timer_Peripheral::b_transport);
        SC_THREAD(timer_tick_thread);
    }
 
private:
    bool running = false;
    unsigned int counter = 0;
    sc_core::sc_event start_event;
 
    void timer_tick_thread() {
        while(true) {
            // Wait for software to enable the timer
            if (!running) wait(start_event);
            
            // Wait 1 microsecond hardware tick
            wait(1, sc_core::SC_US); 
            if (running) counter++;
        }
    }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        sc_dt::uint64 adr = trans.get_address();
        unsigned char* ptr = trans.get_data_ptr();
 
        if (trans.get_command() == tlm::TLM_WRITE_COMMAND) {
            if (adr == 0x00) { 
                // Offset 0x00: Control Register (Write 1 to Start)
                uint32_t val;
                // Strict-aliasing compliant extraction
                memcpy(&val, ptr, sizeof(val));
                running = (val != 0);
                if (running) {
                    std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Started." << std::endl;
                    // Under the hood: this notify() is executing on the CPU's thread!
                    start_event.notify();
                } else {
                    std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Stopped." << std::endl;
                }
            }
        } else if (trans.get_command() == tlm::TLM_READ_COMMAND) {
            if (adr == 0x04) { 
                // Offset 0x04: Counter Register (Read Only)
                memcpy(ptr, &counter, sizeof(counter));
                std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] CPU Read Counter: " << counter << std::endl;
            }
        }
 
        delay += sc_core::sc_time(5, sc_core::SC_NS);
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};
 
// 3. Mock Initiator to Drive the Timer
SC_MODULE(CPU_Driver) {
    tlm_utils::simple_initiator_socket<CPU_Driver> socket;
    SC_CTOR(CPU_Driver) : socket("socket") { SC_THREAD(run); }
 
    void run() {
        tlm::tlm_generic_payload trans;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        uint32_t data = 1; // Start command
        
        // 1. Write 1 to Timer Control Register (Offset 0x00)
        trans.set_command(tlm::TLM_WRITE_COMMAND);
        trans.set_address(0x00);
        trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
        trans.set_data_length(4);
        socket->b_transport(trans, delay);
        wait(delay); 
 
        // 2. Wait for 5 microseconds of simulation time to pass
        wait(5, sc_core::SC_US);
 
        // 3. Read from Timer Counter Register (Offset 0x04)
        delay = sc_core::SC_ZERO_TIME;
        trans.set_command(tlm::TLM_READ_COMMAND);
        trans.set_address(0x04);
        socket->b_transport(trans, delay);
        wait(delay);
    }
};
 
int sc_main(int argc, char* argv[]) {
    CPU_Driver cpu("cpu");
    Timer_Peripheral timer("timer");
    
    // Direct binding for demonstration
    cpu.socket.bind(timer.socket);
    
    sc_core::sc_start();
    return 0;
}

Architectural Design and Thread Context

  • The RAM Module simply allocates memory and uses memcpy to move data directly. It serves as a generic bulk storage endpoint. Notice we use memcpy exclusively. Attempting to cast the payload's unsigned char* ptr to uint32_t* violates C++ strict aliasing rules, causing undefined behavior or crashes depending on the underlying CPU architecture running the simulation.
  • The Hardware Timer Module is modeled around Memory-Mapped Registers. Instead of moving bulk memory, its b_transport acts as an if/else switch statement, triggering specific internal C++ events or threads when a particular offset (e.g., 0x00) is written to.
  • Thread Context Awareness: It is critical to understand that b_transport is just a virtual function call. When the Timer_Peripheral invokes start_event.notify(), that invocation is happening on the CPU's thread context. The event notify() simply injects a wakeup task into the sc_simcontext scheduler. The CPU thread returns from b_transport, yields using wait(delay), and then the scheduler successfully wakes the timer_tick_thread in the next delta cycle.

Comments and Corrections