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.
How to Read This Lesson
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.
Standard and source context
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.
Under the Hood: simple_target_socket C++ Implementation
When you use tlm_utils::simple_target_socket, you are bypassing the need to manually inherit from and implement the tlm::tlm_fw_transport_if.
In the official Accellera repository, simple_target_socket inherits from tlm::tlm_target_socket<BUSWIDTH, TYPES>. It encapsulates an internal nested class called fw_process that actively implements b_transport, nb_transport_fw, get_direct_mem_ptr, and transport_dbg.
When you call socket.register_b_transport(this, &MemoryTarget::b_transport), the socket stores an sc_core::sc_spawn_options and a functor (or member function pointer adapter) inside its internal state.
During simulation, when the initiator calls init_socket->b_transport(trans, delay), it traverses the bound SystemC port array and lands directly on the target's fw_process::b_transport. This internal method checks if a custom b_transport callback was registered. If yes, it dereferences the functor and executes your MemoryTarget::b_transport directly in the execution context of the initiator's thread. This abstraction provides a massive productivity boost while compiling down to zero-overhead C++ virtual function calls.
Source-reading checkpoint
For firmware-visible configuration, inspect .codex-src/cci and the consuming broker path beside the bus model. Configuration only helps when its effect can be traced into runtime behavior.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections