Chapter 12: Virtual Platform Construction
The VP Peripherals
Building RAM and Hardware Timer memory-mapped peripherals using TLM 2.0 Simple Target Sockets.
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.
Complete Peripheral Example
This 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;
memcpy(&val, ptr, sizeof(val));
running = (val != 0);
if (running) {
std::cout << "@" << sc_core::sc_time_stamp() << " [Timer] Started." << std::endl;
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
- The RAM Module simply allocates memory and uses
memcpyto move data directly. It serves as a generic bulk storage endpoint. - 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.
These paradigms form the foundational building blocks of every single peripheral (UARTs, DMA Controllers, Interrupt Controllers) inside a Virtual Platform.
Comments and Corrections