The VP Peripherals
Building RAM and Hardware Timer memory-mapped peripherals using TLM 2.0 Simple Target Sockets.
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
memcpyto move data directly. It serves as a generic bulk storage endpoint. Notice we usememcpyexclusively. Attempting to cast the payload'sunsigned char* ptrtouint32_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_transportacts as anif/elseswitch 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_transportis just a virtual function call. When theTimer_Peripheralinvokesstart_event.notify(), that invocation is happening on the CPU's thread context. The eventnotify()simply injects a wakeup task into thesc_simcontextscheduler. The CPU thread returns fromb_transport, yields usingwait(delay), and then the scheduler successfully wakes thetimer_tick_threadin 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.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections