VP Firmware Traffic and Real-World Flow
A complete TLM-2.0 Loosely Timed (LT) VP scenario with firmware traffic, routing, memory access, and timing.
VP Firmware Traffic and Real-World Flow
The final Virtual Platform (VP) should tell a story that accurately represents real firmware bring-up. In an industrial setting, a VP connects standard bus architectures, executes firmware instructions from a CPU initiator, routes transactions through an interconnect, and manipulates target peripherals.
Boot Sequence
Even with a dummy CPU initiator, we can simulate a standard firmware-like sequence:
- Write a configuration byte to a peripheral.
- Read a status register from memory.
- Advance simulation time correctly based on memory latency.
This demonstrates interconnect routing, target response handling, and Loosely Timed (LT) quantum accumulation.
Standard Doulos / Accellera Interconnect Pattern
To demonstrate this cleanly, we abandon proprietary wrappers and rely exclusively on the IEEE 1666 standard TLM-2.0 b_transport interface and a standard Simple Bus / Interconnect model.
In this architecture:
- An Initiator generates payload transactions.
- A Router (Interconnect) inspects the payload's address and forwards it to the correct target.
- The Targets (Memory, Peripherals) implement the
b_transportinterface, apply latency via thesc_timereference parameter, and return standardTLM_OK_RESPONSEstatuses.
Complete End-to-End Example
The following is a 100% complete, compilable SystemC model of a Loosely Timed VP running a firmware boot traffic scenario over a standard interconnect.
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
using namespace sc_core;
using namespace tlm;
// ---------------------------------------------------------
// Target 1: Memory (Base Address: 0x0000)
// ---------------------------------------------------------
class MemoryTarget : public sc_module {
public:
tlm_utils::simple_target_socket<MemoryTarget> socket;
unsigned char mem[1024];
SC_HAS_PROCESS(MemoryTarget);
MemoryTarget(sc_module_name name) : sc_module(name), socket("socket") {
socket.register_b_transport(this, &MemoryTarget::b_transport);
for(int i=0; i<1024; ++i) mem[i] = 0; // Initialize memory
mem[0x10] = 0xAA; // Pre-load a "Boot Status" value
}
void b_transport(tlm_generic_payload& trans, sc_time& delay) {
tlm_command cmd = trans.get_command();
sc_dt::uint64 addr = trans.get_address();
unsigned char* ptr = trans.get_data_ptr();
unsigned int len = trans.get_data_length();
// Memory target only handles offsets 0x0000 - 0x03FF
if (addr >= 1024) {
trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
return;
}
if (cmd == TLM_READ_COMMAND) {
memcpy(ptr, &mem[addr], len);
} else if (cmd == TLM_WRITE_COMMAND) {
memcpy(&mem[addr], ptr, len);
}
// Apply a realistic memory access latency
delay += sc_time(20, SC_NS);
trans.set_response_status(TLM_OK_RESPONSE);
}
};
// ---------------------------------------------------------
// Target 2: Peripheral (Base Address: 0x1000)
// ---------------------------------------------------------
class UARTPeripheral : public sc_module {
public:
tlm_utils::simple_target_socket<UARTPeripheral> socket;
SC_HAS_PROCESS(UARTPeripheral);
UARTPeripheral(sc_module_name name) : sc_module(name), socket("socket") {
socket.register_b_transport(this, &UARTPeripheral::b_transport);
}
void b_transport(tlm_generic_payload& trans, sc_time& delay) {
tlm_command cmd = trans.get_command();
unsigned char* ptr = trans.get_data_ptr();
if (cmd == TLM_WRITE_COMMAND) {
// Firmware writing to UART TX register
std::cout << "[UART] Transmitting byte: 0x" << std::hex << (int)(*ptr) << std::dec << "\n";
}
// Peripheral access is slower than memory
delay += sc_time(100, SC_NS);
trans.set_response_status(TLM_OK_RESPONSE);
}
};
// ---------------------------------------------------------
// Interconnect: Simple Router
// ---------------------------------------------------------
class Interconnect : public sc_module {
public:
tlm_utils::simple_target_socket<Interconnect> target_socket;
tlm_utils::simple_initiator_socket<Interconnect> init_socket_mem;
tlm_utils::simple_initiator_socket<Interconnect> init_socket_uart;
SC_HAS_PROCESS(Interconnect);
Interconnect(sc_module_name name) : sc_module(name) {
target_socket.register_b_transport(this, &Interconnect::b_transport);
}
void b_transport(tlm_generic_payload& trans, sc_time& delay) {
sc_dt::uint64 addr = trans.get_address();
// Standard address decoding map
if (addr < 0x1000) {
// Route to Memory
init_socket_mem->b_transport(trans, delay);
} else if (addr >= 0x1000 && addr < 0x2000) {
// Route to UART and subtract base address for local offset
trans.set_address(addr - 0x1000);
init_socket_uart->b_transport(trans, delay);
// Restore original address to maintain generic payload contract
trans.set_address(addr);
} else {
trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
}
}
};
// ---------------------------------------------------------
// Initiator: Firmware CPU Wrapper
// ---------------------------------------------------------
class FirmwareCPU : public sc_module {
public:
tlm_utils::simple_initiator_socket<FirmwareCPU> socket;
SC_HAS_PROCESS(FirmwareCPU);
FirmwareCPU(sc_module_name name) : sc_module(name), socket("socket") {
SC_THREAD(execute_firmware);
}
void execute_firmware() {
tlm_generic_payload trans;
sc_time delay = SC_ZERO_TIME;
unsigned char data;
std::cout << "Time " << sc_time_stamp() << ": Firmware booting...\n";
// 1. Read Boot Status from Memory (Address 0x0010)
trans.set_command(TLM_READ_COMMAND);
trans.set_address(0x0010);
trans.set_data_ptr(&data);
trans.set_data_length(1);
trans.set_response_status(TLM_INCOMPLETE_RESPONSE);
socket->b_transport(trans, delay);
if (trans.get_response_status() == TLM_OK_RESPONSE) {
std::cout << "Time " << sc_time_stamp() << ": Read boot status: 0x" << std::hex << (int)data << std::dec << " (Accumulated Delay: " << delay << ")\n";
}
// 2. Consume accumulated delay to sync with SystemC kernel
wait(delay);
delay = SC_ZERO_TIME;
// 3. Write 'O' (0x4F) to UART (Address 0x1000)
data = 0x4F;
trans.set_command(TLM_WRITE_COMMAND);
trans.set_address(0x1000);
trans.set_response_status(TLM_INCOMPLETE_RESPONSE);
socket->b_transport(trans, delay);
wait(delay); // Sync again
std::cout << "Time " << sc_time_stamp() << ": Firmware execution complete.\n";
}
};
// ---------------------------------------------------------
// Top Level
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
FirmwareCPU cpu("cpu");
Interconnect bus("bus");
MemoryTarget mem("mem");
UARTPeripheral uart("uart");
// Bindings
cpu.socket.bind(bus.target_socket);
bus.init_socket_mem.bind(mem.socket);
bus.init_socket_uart.bind(uart.socket);
sc_start();
return 0;
}What This Architecture Demonstrates
A professional VP architect ensures the code communicates real hardware intent:
- Address Decoding: The
Interconnectmodel acts as a router, stripping base addresses to provide the target with a 0-indexed local offset, matching real-world IP block behavior. - Timing Accumulation: In Loosely Timed (LT) models, the initiator (
FirmwareCPU) accumulates time in thedelayvariable duringb_transportcalls, but the SystemC kernel timesc_time_stamp()does not advance untilwait(delay)is explicitly called. This enables blazing-fast simulation without sacrificing causality. - Payload Contracts: Initiators must reset the response status to
TLM_INCOMPLETE_RESPONSEbefore sending, and targets must set it toTLM_OK_RESPONSE(or an error) before returning. - Doulos / Standard Guidelines: The use of
tlm_utils::simple_target_socketcleanly encapsulates interface implementation boilerplate, matching the standard Accellera LT examples.
Comments and Corrections