Chapter 12: Virtual Platform Construction

The VP Peripherals

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

Listen to this lessonAudiobook mode

How to Read This Lesson

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.

Standard and source context

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.

Source-reading checkpoint

For a peripheral model, follow its tlm_target_socket callback into b_transport, then trace register access. This keeps transport mechanics separate from device behavior. If the peripheral is configurable, continue into .codex-src/cci before treating a reset value as fixed policy.

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