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
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.
Comments and Corrections