The VP Router (Interconnect)
Building a TLM-2.0 interconnect to decode addresses and route transactions to peripherals using Doulos Simple Bus concepts.
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 Router
The Router (or Interconnect) is the central address decoder of any Virtual Platform. It sits between the initiators (CPUs, DMAs) and the targets (Memory, Peripherals).
When an initiator sends a read transaction for physical address 0x4000_0004, the Router must determine which target socket is mapped to that address, subtract the target's base address, forward the transaction, and properly restore the address on the return path. Now let's look at how the Accellera TLM kernel enforces this.
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.
The Complete Router Example
This example demonstrates a strictly LRM-compliant, Doulos Simple Bus-style TLM 2.0 Router. It connects to a mock CPU and routes to two generic memory blocks acting as RAM and Timer.
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <vector>
// 1. The Interconnect Router
SC_MODULE(VPRouter) {
// Single target socket facing the CPU
tlm_utils::simple_target_socket<VPRouter> target_socket;
// Array of initiator sockets facing the peripherals
std::vector<tlm_utils::simple_initiator_socket<VPRouter>*> init_sockets;
SC_CTOR(VPRouter) : target_socket("target_socket") {
target_socket.register_b_transport(this, &VPRouter::b_transport);
// Dynamically create two sockets for our 2 peripherals (RAM and Timer)
init_sockets.push_back(new tlm_utils::simple_initiator_socket<VPRouter>("init_ram"));
init_sockets.push_back(new tlm_utils::simple_initiator_socket<VPRouter>("init_timer"));
}
~VPRouter() {
for (auto* socket : init_sockets) delete socket;
}
private:
void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
sc_dt::uint64 original_address = trans.get_address();
sc_dt::uint64 local_address = 0;
int target_port = -1;
// Address Decoding Map
if (original_address >= 0x00000000 && original_address <= 0x0003FFFF) {
// RAM Region (256 KB)
target_port = 0;
local_address = original_address - 0x00000000;
}
else if (original_address >= 0x40000000 && original_address <= 0x40000FFF) {
// Timer Region (4 KB)
target_port = 1;
local_address = original_address - 0x40000000; // Subtract Base Address!
}
if (target_port != -1) {
// Modify the transaction address so the peripheral sees a local offset (0x0 to 0xFFF)
trans.set_address(local_address);
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Router] Routing global addr 0x" << std::hex << original_address
<< " to port " << target_port << " as local addr 0x" << local_address << std::endl;
// Forward transaction
(*init_sockets[target_port])->b_transport(trans, delay);
// Restoring Address (CRITICAL RULE)
trans.set_address(original_address);
} else {
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Router] ERROR: Address 0x" << std::hex << original_address
<< " is unmapped." << std::endl;
trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
}
// Add minimal routing delay
delay += sc_core::sc_time(2, sc_core::SC_NS);
}
};
// 2. Mock CPU Initiator
SC_MODULE(MockCPU) {
tlm_utils::simple_initiator_socket<MockCPU> socket;
SC_CTOR(MockCPU) : 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 = 0;
trans.set_command(tlm::TLM_READ_COMMAND);
trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
trans.set_data_length(4);
// Send a transaction to the Timer base address
trans.set_address(0x40000000);
socket->b_transport(trans, delay);
// Send a transaction to an unmapped address
trans.set_address(0xFFFFFFFF);
socket->b_transport(trans, delay);
}
};
// 3. Mock Peripheral
SC_MODULE(MockPeripheral) {
tlm_utils::simple_target_socket<MockPeripheral> socket;
SC_CTOR(MockPeripheral) : socket("socket") {
socket.register_b_transport(this, &MockPeripheral::b_transport);
}
void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
trans.set_response_status(tlm::TLM_OK_RESPONSE);
}
};
// 4. Top Level
int sc_main(int argc, char* argv[]) {
MockCPU cpu("cpu");
VPRouter router("router");
MockPeripheral ram("ram");
MockPeripheral timer("timer");
// Bindings
cpu.socket.bind(router.target_socket);
(*router.init_sockets[0]).bind(ram.socket);
(*router.init_sockets[1]).bind(timer.socket);
sc_core::sc_start();
return 0;
}The Address Restoration Rule
A target peripheral should never care that it is mapped at 0x4000_0000. It only cares about its internal offsets (e.g., register 0x00 is Control, 0x04 is Counter). This allows the IP block to be perfectly reusable across any SoC project. The Router achieves this by passing original_address - base_address.
However, the CPU initiator expects the payload it sent to return unaltered.
Under the Hood: In the tlm_generic_payload implementation, set_address(uint64_t) literally overwrites the internal m_address integer field. Therefore, after the target returns from b_transport, the router must immediately restore the original address. If it fails to do so, advanced initiators that inspect returning payloads to match transaction queues will crash or corrupt simulation memory.
Error Handling
If an address doesn't match any mapped region, the Router immediately assigns tlm::TLM_ADDRESS_ERROR_RESPONSE and returns. Under the Hood: This is simply an assignment to the m_response_status enum field in the payload. The initiator CPU will inspect this response and typically trigger a hardware exception (e.g., an ARM Data Abort) inside its Instruction Set Simulator (ISS).
DMI Routing (Direct Memory Interface)
While not shown in this simple example, production routers also implement get_direct_mem_ptr.
Under the Hood: If the CPU requests DMI, the router passes the request down to the target memory. The target returns a tlm_dmi struct containing a raw unsigned char* pointer and its local bounds (e.g., 0x0 to 0x3FFFF). Before the router returns this struct to the CPU, it MUST add the base address back into the DMI bounds (e.g., 0x00000000 to 0x0003FFFF). This allows the CPU's Instruction Set Simulator to directly pointer-dereference addresses in the global map without invoking the socket hierarchy, speeding up simulation by orders of magnitude.
Comments and Corrections