VP Architecture Review for Technical Leads
A lead-engineer checklist and compliant architectural example for reviewing a SystemC virtual platform before deployment.
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.
VP Architecture Review for Technical Leads
This page is written for the technical lead who must decide whether a Virtual Platform (VP) is ready for firmware bring-up, architecture exploration, or customer release. A VP is only useful if it acts as a reliable, unambiguous contract between hardware designers and software engineers.
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 Review Checklist
Before a VP is deployed, it must be evaluated against the following criteria:
1. Memory Map Contract
Check that every region has a strictly defined:
- Base address and size (preventing overlaps and undefined holes).
- Access width policy and Endianness.
- Error response policy (what happens on unmapped accesses?).
- Debug transport support (
transport_dbg).
2. Register Quality
If a firmware engineer cannot write a driver from the documentation and the model's behavior, the VP is incomplete. Every peripheral must enforce:
- Reset values and reserved bits.
- Read-only vs Write-only semantics.
- Side effects (e.g., clear-on-read).
- Interrupt generation policies.
3. Timing Contract
The VP must state exactly what simulation time means. Examples:
- "RAM access latency is modeled as a constant 20ns TLM delay."
- "UART transmission is one character delay per byte."
- "Interrupt propagation uses SystemC
sc_signalupdate semantics."
4. Configuration and Observability
- CCI parameters must be named stably, documented, and properly locked if structural.
- The VP must detect unconsumed presets.
- Use standardized SystemC reporting macros (
SC_REPORT_INFO,SC_REPORT_FATAL) instead of rawstd::cout.
Complete Example: The "Review-Ready" Compliant Block
The following complete, compilable example demonstrates a peripheral that strictly adheres to the review criteria above. It models a compliant timer peripheral with strict register semantics, debug transport, CCI configuration, proper memory bounds checking, and explicitly stated timing contracts.
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <cci_configuration>
using namespace sc_core;
using namespace tlm;
class CompliantTimer : public sc_module {
public:
// Memory map socket
tlm_utils::simple_target_socket<CompliantTimer> socket;
// Interrupt out
sc_out<bool> irq_out;
// CCI Configuration (Review #4)
cci::cci_param<int> base_frequency_hz;
SC_HAS_PROCESS(CompliantTimer);
CompliantTimer(sc_module_name name)
: sc_module(name)
, socket("socket")
, irq_out("irq_out")
, base_frequency_hz("base_frequency_hz", 1000, "Base clock frequency in Hz")
{
socket.register_b_transport(this, &CompliantTimer::b_transport);
socket.register_transport_dbg(this, &CompliantTimer::transport_dbg);
SC_THREAD(timer_process);
}
private:
// Registers (Review #2: Register Quality)
// 0x00: CTRL (Bit 0: Enable, Bit 1: Interrupt Enable)
// 0x04: STATUS (Bit 0: Timer Fired - Clear on Write 1)
uint32_t reg_ctrl = 0;
uint32_t reg_status = 0;
sc_event ev_timer_fired;
// Review #3: Timing Contract.
// Register access is modeled as a fixed 10ns delay.
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();
// Review #1: Memory Map Contract (Bounds and access width checking)
if (addr > 0x04 || len != 4) {
trans.set_response_status(TLM_ADDRESS_ERROR_RESPONSE);
return;
}
if (cmd == TLM_READ_COMMAND) {
uint32_t val = (addr == 0x00) ? reg_ctrl : reg_status;
memcpy(ptr, &val, 4);
} else if (cmd == TLM_WRITE_COMMAND) {
uint32_t val;
memcpy(&val, ptr, 4);
if (addr == 0x00) {
reg_ctrl = val & 0x03; // Mask reserved bits
SC_REPORT_INFO("Timer", "CTRL register updated.");
} else if (addr == 0x04) {
// Clear on write 1
if (val & 0x01) {
reg_status &= ~0x01;
irq_out.write(false);
}
}
}
delay += sc_time(10, SC_NS); // Apply timing contract
trans.set_response_status(TLM_OK_RESPONSE);
}
// Review #1 & #6: Debug transport bypasses delays and side-effects
unsigned int transport_dbg(tlm_generic_payload& trans) {
sc_dt::uint64 addr = trans.get_address();
if (addr > 0x04 || trans.get_data_length() != 4) return 0;
uint32_t val = (addr == 0x00) ? reg_ctrl : reg_status;
memcpy(trans.get_data_ptr(), &val, 4);
return 4; // Bytes read
}
void timer_process() {
while (true) {
// Wait for 1 tick based on CCI parameter
sc_time tick_period(1.0 / base_frequency_hz.get_value(), SC_SEC);
wait(tick_period);
if (reg_ctrl & 0x01) { // If Enabled
reg_status |= 0x01; // Set status
if (reg_ctrl & 0x02) { // If Interrupt Enabled
irq_out.write(true);
}
}
}
}
};
// --- Top Level Testbench ---
int sc_main(int argc, char* argv[]) {
// Setup Broker (Review #4)
cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
// Instantiate and bind
CompliantTimer timer("timer");
sc_signal<bool> sig_irq;
timer.irq_out(sig_irq);
// Dummy transaction to test the contract
tlm_generic_payload trans;
sc_time delay = SC_ZERO_TIME;
uint32_t data = 0x03; // Enable + IRQ Enable
trans.set_command(TLM_WRITE_COMMAND);
trans.set_address(0x00);
trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
trans.set_data_length(4);
trans.set_response_status(TLM_INCOMPLETE_RESPONSE);
// Send transaction (Simulating a CPU)
timer.socket->b_transport(trans, delay);
wait(delay); // Accumulate time
if (trans.get_response_status() == TLM_OK_RESPONSE) {
SC_REPORT_INFO("CPU", "Successfully configured timer.");
}
sc_start(2, SC_MS); // Run to see IRQ fire
return 0;
}The Approval Bar
A Virtual Platform is considered "lead-review ready" only when:
- Software-visible behaviors (registers, interrupts) perfectly match documentation.
- Extraneous configuration parameters are locked.
- The boundary between Loosely Timed (LT) approximations and Cycle Accurate (CA) behavior is formally recorded.
- Debug inspection (
transport_dbg) operates independently from time-advancing data paths.
Under the Hood: transport_dbg and sc_signal Semantics
When reviewing a VP architecture, two C++ implementation details frequently cause subtle simulation bugs: transport_dbg violations and interrupt signal semantics.
1. The transport_dbg Contract
The tlm_fw_transport_if explicitly separates b_transport from transport_dbg. Under the hood, transport_dbg is a purely synchronous, non-blocking C++ function call that lacks an sc_time argument. It is illegal to call sc_core::wait() or modify the target's internal state (e.g., clearing a FIFO or a "clear-on-read" register bit) inside transport_dbg. Backdoor tools like GDB debuggers invoke transport_dbg directly through the interconnect. If it modifies state, the debugger observing memory will permanently corrupt the simulation timeline.
2. Interrupt Propagation via sc_signal
In the compliant example above, irq_out.write(true) is used. In the SystemC kernel, sc_signal::write() does not immediately change the signal's value. Instead, it schedules an update() request in the simulation kernel's event queue. The new value is applied during the Update Phase at the end of the current delta cycle.
If the CPU initiator models its interrupt polling within a single b_transport quantum without yielding back to the SystemC scheduler via wait(), it will completely miss the interrupt. VPs must explicitly state whether they use TLM payload interrupts (immediate execution) or sc_signal pins (requires a delta cycle yield).
Deep Dive: Accellera Source for sc_signal and update()
The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.
The write() Implementation
When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:
template<class T>
inline void sc_signal<T>::write(const T& value_) {
if( !(m_new_val == value_) ) {
m_new_val = value_;
this->request_update(); // Inherited from sc_prim_channel
}
}The request_update() call appends the channel to sc_simcontext::m_update_list.
The update() Phase
After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:
template<class T>
inline void sc_signal<T>::update() {
if( !(m_new_val == m_cur_val) ) {
m_cur_val = m_new_val;
m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
}
}This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.
Comments and Corrections