TSystemC Programmer Guide
Version 1.0.0 · Windows x64 · © 2026 Muhammad Anisur Rahman
TSystemC is a SystemC simulator. It implements the SystemC class library of IEEE Std 1666-2023 — kernel, channels, data types, fixed-point types, tracing, TLM-2.0 and TLM-1 — and adds a power modeling library, sc_power. Models written for the standard API compile against TSystemC unchanged.
Besides the C++ library, TSystemC ships:
tsystemc, a command line that compiles and runs models, steps them from scripts or an interactive prompt, and serves them to AI agents over MCP;tsystemc.dll, a C API through which any program — a test bench, a Verilog simulator through DPI-C, a script interpreter — can load a model, advance time, and read and write its signals and power state.
This guide is organised as follows:
- Installation
- Your first model
- Building models
- SystemC essentials
- Transaction-level modeling (TLM-2.0)
- Power modeling
- The tsystemc command line
- Embedding: the C host API
- Co-simulation and AI agents
- Examples
- Conformance notes
- Troubleshooting
- Power API reference
1. Installation
What you need
- Windows 10 or 11, 64-bit.
- A C++17 compiler. You need one of these:
- g++ from TDM-GCC 10.3.0 (64-bit), the compiler the release is built and tested with. Other MinGW-w64 GCC 10+ builds that use the MSVCRT runtime with SEH exceptions should also work, but they are not tested.
- Microsoft Visual C++: the package includes
lib\sysc_msvc.lib, built with Visual Studio 2026 using/MD. Link it with the same or a newer toolset.
The tsystemc command line compiles models with g++, so it needs g++ on the PATH, or named in TSYSTEMC_CXX.
Unpacking
Unpack the archive anywhere. For example:
C:\tools\tsystemc-1.0.0\
bin\ tsystemc.exe, tsystemc.dll
include\ systemc.h, systemc, tlm.h, tlm, sc_power.h, sc_power_tlm.h, sysc_api.h, ...
lib\ libsysc.a, sce_model_entry.o, libtsystemc_api.a (g++)
sysc_msvc.lib, sce_model_entry_msvc.obj (MSVC)
examples\ runnable examples with their expected output
docs\ this guide (Markdown and HTML)
Add bin to your PATH. tsystemc finds the headers and libraries from its own location, as the parent of bin. Set TSYSTEMC_HOME only if you move tsystemc.dll away from the rest of the package.
Check the installation:
> tsystemc version
TSystemC 1.0.0 (IEEE 1666-2023 SystemC, TLM-2.0, power modeling; model ABI v2)
Environment variables
| Variable | Meaning |
|---|---|
TSYSTEMC_HOME | Installation root (the directory holding include and lib). Default: the parent of the directory holding tsystemc.dll. |
TSYSTEMC_CXX | The compiler tsystemc runs (default g++). |
SYSC_CXXFLAGS | Extra compiler flags for models that tsystemc compiles. |
SCE_STACK_SIZE | Default stack size of thread processes, in bytes (default 256 KiB). |
2. Your first model
Save this as counter.cpp:
#include <systemc.h>
SC_MODULE(Counter) {
sc_in<bool> clk, reset;
sc_out<sc_uint<8>> count;
SC_CTOR(Counter) {
SC_METHOD(tick);
sensitive << clk.pos();
}
void tick() {
count.write(reset.read() ? 0 : count.read() + 1);
}
};
int sc_main(int, char *[]) {
sc_clock clk("clk", 10, SC_NS);
sc_signal<bool> reset("reset");
sc_signal<sc_uint<8>> count("count");
Counter c("c");
c.clk(clk);
c.reset(reset);
c.count(count);
sc_trace_file *tf = sc_create_vcd_trace_file("counter");
sc_trace(tf, clk, "clk");
sc_trace(tf, count, "count");
reset = true;
sc_start(25, SC_NS);
reset = false;
sc_start(100, SC_NS);
std::cout << sc_time_stamp() << ": count = " << count.read() << std::endl;
sc_close_vcd_trace_file(tf);
return 0;
}
Run it:
> tsystemc run counter.cpp
125 ns: count = 10
tsystemc run compiles the model the first time and caches the result in %TEMP%\tsystemc_cache. Later runs reuse the cached model until the source file or the library changes. counter.vcd opens in any waveform viewer, such as GTKWave.
To build a normal executable instead:
> tsystemc build counter.cpp -o counter.exe
> counter.exe
or with g++ directly (see the next chapter).
3. Building models
With g++
g++ -std=c++17 -O2 -I%TSYSTEMC_HOME%\include model.cpp %TSYSTEMC_HOME%\lib\libsysc.a -static -o model.exe
libsysc.acontains the whole engine and amain()that calls yoursc_main, as in any SystemC implementation.-staticgives an executable with no runtime DLLs.- C++17 is required.
With Microsoft Visual C++
From a "x64 Native Tools" command prompt:
cl /std:c++17 /EHsc /MD /I%TSYSTEMC_HOME%\include model.cpp %TSYSTEMC_HOME%\lib\sysc_msvc.lib /link /SUBSYSTEM:CONSOLE
main() lives in the library, so the linker must be told the subsystem explicitly.
Model DLLs
A model DLL is a model compiled together with the engine and a small entry shim (sce_model_entry.o). A host loads it to drive the model; see chapters 7 and 8. tsystemc builds model DLLs for you:
tsystemc compile model.cpp -o model.dll
You only need a model DLL when another program will load the model. To run it yourself, use tsystemc run or an executable.
Headers
| Include | Provides |
|---|---|
<systemc.h> | SystemC, with the standard names brought into the global namespace (the classic style) |
<systemc> | SystemC in sc_core / sc_dt only |
<tlm.h> / <tlm> | TLM-2.0 and TLM-1 (namespace tlm) |
<tlm_utils/...> | Convenience sockets, payload event queues, quantum keeper, instance-specific extensions |
<sc_power.h> | The power modeling library (namespace sc_power) |
<sc_power_tlm.h> | Power modeling plus its TLM-2.0 helpers |
<sysc_api.h> | The C host API of tsystemc.dll |
Fixed-point types are available after #define SC_INCLUDE_FX before the include, as the standard requires.
4. SystemC essentials
This chapter is a working summary of SystemC as TSystemC implements it. IEEE Std 1666-2023 is the normative reference.
4.1 Elaboration and simulation
A SystemC program has two phases.
- Elaboration.
sc_mainconstructs modules, channels and ports and binds them. Constructors register processes. Nothing simulates yet. - Simulation. It starts at the first
sc_start(). The kernel callsend_of_elaborationandstart_of_simulationon every module, channel, port and export, then runs the scheduler.
Calling sc_start(t) repeatedly continues the same simulation. sc_start() with no argument runs until no activity remains, or until a process calls sc_stop().
The scheduler runs in evaluate–update–delta cycles:
- Every runnable process runs once (evaluate).
- Primitive channels then commit their new values (update).
- Processes sensitive to the resulting changes run in the next delta cycle, at the same simulation time.
Time advances only when no delta activity is left.
4.2 Modules and processes
SC_MODULE(Filter) {
sc_in<bool> clk;
sc_in<int> in;
sc_out<int> out;
sc_event wake{"wake"};
SC_CTOR(Filter) {
SC_METHOD(compute); // runs to completion each time it is triggered
sensitive << in;
SC_THREAD(control); // has its own stack; may call wait()
SC_CTHREAD(sample, clk.pos()); // a clocked thread
}
void compute() { out.write(in.read() / 2); }
void control() {
for (;;) {
wait(100, SC_NS);
wake.notify();
}
}
void sample() {
for (;;) {
wait(); // the next rising clock edge
...
}
}
};
SC_METHODprocesses cannot suspend. They usenext_trigger()to change their sensitivity for the next activation.SC_THREADandSC_CTHREADprocesses suspend withwait(): on time, on events, on event lists (e1 | e2,e1 & e2), or on the static sensitivity.dont_initialize()keeps a process from running at time zero.- Dynamic processes are created with
sc_spawn, before or during simulation. Passsc_spawn_optionsto make a method, set sensitivity, or set a reset. - Process control works through
sc_process_handle:suspend,resume,disable,enable,kill,reset,throw_it,sync_reset_on/off, andreset_signal_is/async_reset_signal_ison threads.
4.3 Channels
| Channel | Use |
|---|---|
sc_signal<T> | Hardware wire or register. Has value-change, positive-edge and negative-edge events (for bool and sc_logic). Writer policies: SC_ONE_WRITER, SC_MANY_WRITERS, SC_UNCHECKED_WRITERS. |
sc_buffer<T> | Like sc_signal, but every write is an event, even when the value does not change. |
sc_clock | Periodic bool signal with configurable period, duty cycle, start time and first edge. |
sc_fifo<T> | Bounded FIFO with blocking and non-blocking access. |
sc_mutex, sc_semaphore | Mutual exclusion and counting semaphores. |
sc_signal_resolved, sc_signal_rv<N> | Multi-driver resolved logic. |
sc_event_queue | Queue of notifications. |
Ports and exports: sc_in, sc_out, sc_inout, sc_fifo_in, sc_fifo_out, sc_port<IF, N, policy>, and sc_export<IF>. To leave a port unconnected on purpose, use sc_unbound, sc_tie or sc_stub.
4.4 Data types
bool,sc_logic(0 1 X Z).sc_int<W>/sc_uint<W>up to 64 bits, andsc_bigint<W>/sc_biguint<W>of any width.sc_bv<W>andsc_lv<W>, with bit and part selects, concatenation and reduction operators.- The fixed-point types
sc_fixed,sc_ufixed,sc_fixandsc_ufix, with every quantization and overflow mode (#define SC_INCLUDE_FX).
4.5 Time
sc_time(10, SC_NS), or sc_time("10 ns"). Units go from SC_YS to SC_SEC. The time resolution defaults to 1 ps. Change it with sc_set_time_resolution before the first sc_time is constructed.
4.6 Reporting
SC_REPORT_INFO("my/model", "starting");
SC_REPORT_WARNING("my/model", "FIFO almost full");
SC_REPORT_ERROR("my/model", "illegal address"); // default action: throw
sc_report_handler::set_actions("my/model", SC_WARNING, SC_DO_NOTHING);
Severities, actions, verbosity levels, stop_after limits and custom handlers all work as the standard defines.
4.7 Tracing
sc_trace_file *tf = sc_create_vcd_trace_file("waves"); // writes waves.vcd
sc_trace(tf, signal, "name");
tf->set_time_unit(1, SC_NS);
...
sc_close_vcd_trace_file(tf);
You can trace any signal, port, variable, data type, fixed-point value, sc_time or sc_event. Values that are not integers are written as VCD reals.
4.8 Naming
Every object and event has a hierarchical name, top.cpu.fifo, from the names passed to the constructors. Look objects up with sc_find_object and sc_find_event.
Give events names (sc_event done{"done"};). As IEEE 1666-2023 §5.10.4 requires, two unnamed events in the same module both get the name "event", and the clash produces a warning.
5. Transaction-level modeling (TLM-2.0)
TLM models communication as function calls carrying a transaction (the generic payload), rather than as signal changes. It is the usual level for virtual platforms, architecture exploration and software development before RTL exists, and it runs orders of magnitude faster than signal-level models.
5.1 Sockets and the generic payload
An initiator (CPU, DMA) owns an initiator socket. A target (memory, peripheral) owns a target socket. Binding them connects the forward path (b_transport, nb_transport_fw, get_direct_mem_ptr, transport_dbg) and the backward path (nb_transport_bw, invalidate_direct_mem_ptr).
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
struct Memory : sc_module {
tlm_utils::simple_target_socket<Memory> socket{"socket"};
unsigned char mem[1024];
SC_CTOR(Memory) { socket.register_b_transport(this, &Memory::b_transport); }
void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
if (t.get_address() + t.get_data_length() > sizeof mem) {
t.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
return;
}
if (t.is_read()) memcpy(t.get_data_ptr(), mem + t.get_address(), t.get_data_length());
else memcpy(mem + t.get_address(), t.get_data_ptr(), t.get_data_length());
delay += sc_time(10, SC_NS); // annotate, do not wait
t.set_response_status(tlm::TLM_OK_RESPONSE);
}
};
The generic payload carries the following:
- Command, address and data: command, address, data pointer and length, byte enables, streaming width.
- Status and DMI flag: the response status and the DMI hint.
- Extensions: any number of these.
The initiator sets the response status to TLM_INCOMPLETE_RESPONSE before each call. The target sets the final status.
5.2 Loosely timed (LT) modeling
The LT style uses the blocking b_transport with timing annotation. The target adds its latency to delay, and the initiator decides when to synchronise.
Temporal decoupling lets an initiator run ahead of simulation time by up to a global quantum before it yields. This is what makes LT platforms fast:
tlm_utils::tlm_quantumkeeper qk;
tlm::tlm_global_quantum::instance().set(sc_time(1, SC_US));
qk.reset();
...
sc_time delay = qk.get_local_time();
socket->b_transport(t, delay);
qk.set(delay);
if (qk.need_sync()) qk.sync();
DMI (direct memory interface) lets an initiator get a raw pointer into a target's memory and bypass transport calls. Debug transport (transport_dbg) reads and writes without side effects or timing.
examples/tlm_lt shows all four: a CPU with a quantum keeper, a router, DMI and debug access.
5.3 Approximately timed (AT) modeling
The AT style uses the non-blocking nb_transport_fw/nb_transport_bw calls and the four phases of the base protocol:
initiator target
BEGIN_REQ ----------------->
<----------------- END_REQ (request accepted; the request channel is free)
<----------------- BEGIN_RESP (response ready)
END_RESP ----------------->
Each call returns:
| Return | Meaning |
|---|---|
TLM_ACCEPTED | The callee took the phase. The answer comes later, on the other path. |
TLM_UPDATED | The callee moved the transaction to a new phase, given in the arguments. |
TLM_COMPLETED | The transaction is finished. No further phases follow. |
Rules that keep AT models correct:
- Annotated delays. Use a payload event queue (
tlm_utils::peq_with_cb_and_phase) to turn the annotated delay of each call into a timed callback, rather than callingwait()insidenb_transport. - Back-pressure. At most one request and one response may be in progress on a socket. An initiator waits for
END_REQbefore the nextBEGIN_REQ. A target delaysEND_REQto apply back-pressure. - Payload lifetime. Payloads must outlive the call. Use a memory manager (
tlm::tlm_mm_interface), andacquire()/release()the payload while you hold it. The manager'sfree()is called when the reference count drops to zero. - Custom phases. Declare private phases with
TLM_DECLARE_EXTENDED_PHASE(name).
examples/tlm_at follows a transaction through every one of these cases.
5.4 Interconnects
A bus or router has one target socket per initiator and one initiator socket per target, or uses the multi-passthrough sockets. These bind any number of times, and the callbacks receive the index of the binding:
tlm_utils::multi_passthrough_target_socket<Bus> targ{"targ"};
tlm_utils::multi_passthrough_initiator_socket<Bus> init{"init"};
...
void b_transport(int port, tlm::tlm_generic_payload &t, sc_time &delay) {
unsigned target = decode(t.get_address());
t.set_address(t.get_address() - base[target]);
init[target]->b_transport(t, delay);
}
An interconnect must translate addresses on the way to the target, restore them on the way back, and adjust DMI ranges. examples/tlm_bus adds arbitration with sc_mutex and publishes every transfer through a TLM-1 analysis port.
5.5 Extensions
struct cpu_id : tlm::tlm_extension<cpu_id> {
unsigned id = 0;
tlm_extension_base *clone() const override { return new cpu_id(*this); }
void copy_from(const tlm_extension_base &e) override { id = static_cast<const cpu_id &>(e).id; }
};
t.set_extension(&ext); // initiator
t.get_extension(p); // anyone on the path; p == nullptr if absent
t.clear_extension(&ext); // before ext goes out of scope
Ignorable extensions (an interconnect may pass them through without understanding them) are the standard way to carry side-band information. sc_power::power_extension is one (see 6.7). tlm_utils::instance_specific_extensions attach private data per module.
5.6 TLM-1
tlm_fifo<T>, put/get/peek and transport interfaces, and tlm_analysis_port with tlm_analysis_fifo are all available. Analysis ports are the usual way to feed monitors, scoreboards and coverage collectors.
6. Power modeling
sc_power adds power and energy to any SystemC model, at signal level or transaction level. It is an extension, not part of IEEE 1666. It answers the questions architects ask before silicon exists:
- How much energy does this workload take?
- Which block dominates?
- What do DVFS, clock gating, power gating and sleep states save?
- What do they cost in latency?
#include <sc_power.h> // or <sc_power_tlm.h> for the TLM helpers
using namespace sc_power;
6.1 Concepts
Power domain (power_domain) — a supply rail, which has:
- a voltage, and optionally a clock period;
- a list of operating points (named voltage/frequency pairs) for DVFS;
- an on/off switch for power gating, with optional switching energy;
- optionally a parent domain: a domain is off whenever its parent is off.
Power model (power_model) — the power behaviour of one component:
- States, each with a static (leakage) power and a dynamic power, for example run / idle / sleep.
- Transitions between states, each with a latency and an energy.
- Operations that cost energy each time they happen, for example an instruction, a memory read, or a MAC. An operation can also have an energy per byte.
- Policies: auto-wake on work, and auto-sleep after a timeout.
DVFS clock (dvfs_clock) — an sc_signal<bool> clock that follows its domain's clock period and stops while the domain is off. Use it where an sc_clock would be used.
Power monitor (power_monitor) — the registry of all models and domains. It prints reports, samples power into CSV, and traces it into VCD.
6.2 Units and scaling
Volts, watts, joules and seconds throughout. Every number you give a model is at the domain's nominal voltage Vnom and nominal clock period. When the domain runs at another operating point, the model scales automatically:
| Quantity | Scales as | Reason |
|---|---|---|
| static (leakage) power | × V/Vnom | leakage current is roughly constant over the DVFS range |
| state dynamic power | × (V/Vnom)² × f/fnom | P = αCV²f |
| operation energy | × (V/Vnom)² | E = CV² per switching event |
| transition energy | not scaled | it is a property of the transition |
If a domain has no clock (nominal period zero), frequency scaling is off and only voltage scaling applies. A model with no domain runs at its nominal values and is always on.
Energy is integrated exactly between events: whenever the state, the voltage, the frequency or the on/off status changes, the energy accumulated so far is booked at the old power, and the new power applies from then on. There is no sampling error.
6.3 A first power model
struct Core : sc_module {
power_domain dom{"dom", 1.0, sc_time(10, SC_NS)}; // 1.0 V, 100 MHz nominal
power_model power{"power", dom};
dvfs_clock clk{"clk", dom};
SC_CTOR(Core) {
power.add_state("run", 2e-3, 18e-3) // 2 mW leakage + 18 mW dynamic
.add_state("idle", 2e-3) // clock gated: leakage only
.add_state("sleep", 0.2e-3) // retention
.add_transition("idle", "sleep", sc_time(500, SC_NS), 10e-9)
.add_transition("sleep", "run", sc_time(2, SC_US), 40e-9)
.add_operation("instr", 50e-12); // 50 pJ per instruction
SC_THREAD(work);
}
void work() {
power.consume("instr", 0, 1000); // 1000 instructions
wait(dom.cycles(1000)); // at one per cycle
power.enter_state("idle"); // charges the transition, waits its latency
...
}
};
- States. The first state added is the initial state. Override it with
set_initial_state. set_state(s)changes state immediately, charges the transition energy, and returns the transition latency. Use it from methods, or when you want to handle the latency yourself.enter_state(s)does the same and then waits for the latency. It can only be called from a thread.- The latency counts in the new state. For example, a 2 µs wake-up is spent drawing
runpower. - Transitions without an entry have zero latency and zero energy.
6.4 Wake and sleep policies
power.set_wake_state("run"); // work wakes the model
power.set_idle_policy(sc_time(3, SC_US), "sleep", "idle"); // 3 us in idle -> sleep
- Auto-wake. With a wake state set,
consume()in any other state first moves the model to the wake state. It returns that transition's latency, so the caller can add it to its delay or wait for it:sc_time wake = power.consume("instr", 0, 100); if (wake != SC_ZERO_TIME) wait(wake); - Auto-sleep.
set_idle_policy(timeout, sleep, from): once the model has spenttimeoutin statefromwithout anyconsume(), it moves tosleep. The defaultfromis the wake state.This is how DRAM self-refresh, cache drowsy modes, or an OS idle governor are modelled. The timer restarts on every operation and every entry into
from.
6.5 DVFS
dom.add_operating_point("turbo", 1.10, sc_time(1, SC_NS))
.add_operating_point("normal", 0.90, sc_time(1.0 / 0.6, SC_NS))
.add_operating_point("eco", 0.70, sc_time(1.0 / 0.3, SC_NS));
dom.set_operating_point("eco");
- Every model in the domain re-scales immediately. Every
dvfs_clockin the domain takes the new period from its next rising edge. dom.cycles(n)converts cycles to time at the current frequency, so a thread that waitsdom.cycles(n)automatically slows down at a lower operating point.set_voltage(v)andset_clock_period(p)set the voltage or the period directly (the operating point then reads "custom").- Built-in point. The nominal point is registered as "nominal".
examples/power_dvfs runs the same deadline-driven workload on two cores, one fixed at the top operating point and one with a governor that picks the slowest point that still meets each deadline, and compares their energy.
6.6 Power gating and clock gating
dom.set_switch_energy(150e-9, 20e-9); // energy to switch on / off
dom.power_off();
...
dom.power_on();
While a domain is off:
- Its models draw nothing. Every model in it, and in its child domains, has zero power and reports
powered() == false. - Calls are not charged.
consume()charges nothing and reports anSC_POWER/offwarning. The first five warnings per model are reported. - TLM accesses fail.
charge()(see the next section) answers TLM transactions withTLM_GENERIC_ERROR_RESPONSE. - Its
dvfs_clocks stop. They hold low after completing the cycle in progress, and restart when the domain comes back.
Clock gating of a single block without switching the domain off is modelled with states: an idle state with leakage only. To stop the block's own clock signal as well, use dvfs_clock::disable()/enable().
6.7 Power in TLM models
<sc_power_tlm.h> provides two pieces.
charge(model, trans, delay) — call it at the top of a target's b_transport (or when an AT target accepts a request):
void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
if (!sc_power::charge(power, t, delay)) return; // domain off: error response set
... normal target behaviour ...
}
It:
- Charges an operation. It charges the model's operation
"read"or"write", or"access"if the specific one is not defined, with the payload's data length as the byte count. - Adds wake-up latency. If the model was asleep, the wake-up latency is added to
delay. - Handles the off state. If the domain is off, it sets
TLM_GENERIC_ERROR_RESPONSEand returns false.
power_extension — an ignorable payload extension. An initiator that attaches it learns what each transaction cost along its whole path:
sc_power::power_extension ext;
t.set_extension(&ext);
socket->b_transport(t, delay);
t.clear_extension(&ext);
// ext.energy joules charged by every target and interconnect on the path
// ext.wake_latency wake-up time they added
// ext.charges how many components charged
Interconnects call charge() too, to account for their own energy per transfer. examples/power_tlm_soc builds a complete SoC this way:
- a CPU with DVFS and sleep;
- a bus;
- a DRAM with self-refresh;
- a power-gated accelerator.
It reports per-frame transaction energy against total system energy.
6.8 Reports, CSV and waveforms
power_monitor &mon = power_monitor::instance();
mon.report(std::cout); // now
mon.report_at_end(); // automatically at the end of simulation
mon.sample(sc_time(1, SC_US), "power.csv"); // before sc_start
mon.trace(tf); // sampled power into a VCD (needs sample())
sc_power::sc_trace(tf, model, "cpu_power"); // a model's background power
The report has one row per model: state, current power, average power, energy, and the static / dynamic / transition split. Then for each model it gives the share of time spent in each state and the count of each operation, and for each domain its voltage, frequency, operating point, power and energy. A * after a state means the model's domain is off.
Power report at 74012 ns
model state power avg power energy static dynamic transitions
cpu.power sleep 225 uW 19.8 mW 1.466 uJ 132.7 nJ 1.315 uJ 18 nJ (5)
dram.power self_refresh 3 mW 13.31 mW 985.4 nJ 519.6 nJ 435.8 nJ 30 nJ (13)
accel.power ready* 0 W 3.719 mW 275.3 nJ 33.84 nJ 241.4 nJ 0 J (6)
total 4.225 mW 45.71 mW 3.383 uJ (domain switching 530 nJ)
The CSV has one row per sample period. Each row has the time in seconds, then each model's average power over that period (dynamic operation energy included), then the total, so it plots directly in a spreadsheet. trace(tf) writes the same sampled values into a VCD as reals.
sc_trace(tf, model, name) traces the model's instantaneous background power: static plus state-dynamic, excluding the per-operation energy, which has no duration.
6.9 From the host
Power models and domains are visible to the host tools by their hierarchical names:
tsystemc> power full report
tsystemc> power core.power core.dom one line each
tsystemc> list power
tsystemc> set core.dom eco switch operating point
tsystemc> set core.dom off gate the domain
tsystemc> set core.power sleep force a state
get core.power returns state=run power=20 mW avg=24.08 mW energy=602 nJ. The C API has sysc_power_report(), and the MCP server has a power tool.
6.10 Calibrating a model
The library computes energy exactly from the numbers you give it. The accuracy of the result is the accuracy of those numbers. Typical sources:
- Static and dynamic power per state: datasheets, or power analysis of RTL or gate-level netlists, for example from a characterisation run with switching activity.
- Energy per operation: the dynamic energy of a characterisation workload divided by the operation count. Or published per-access energies for memories (for example from CACTI-style models).
- Transition latency and energy: power-management IP documentation, or measurements.
Keep each model's numbers at one documented nominal point, and let the domain scale them.
6.11 Limitations
- Leakage is modelled as linear in voltage and independent of temperature. There is no thermal model.
- Operation energy is booked at the instant of
consume(). Spread long operations over several calls if you need a smooth power trace. - The CSV and VCD power traces are averages over the sample period. Choose a period shorter than the effects you want to see.
- There is no UPF/IEEE 1801 import. Domains and states are described in C++.
7. The tsystemc command line
tsystemc run <model.cpp|dll> [--time T] [--vcd F] [--trace names] [-- args...]
tsystemc build <model.cpp> [-o model.exe] [--cxxflags "..."]
tsystemc compile <model.cpp> [-o model.dll] [--cxxflags "..."]
tsystemc script <file.scs> run a command script (exit code = failures)
tsystemc -c "<cmd>; <cmd>; ..." inline script
tsystemc interactive prompt
tsystemc mcp MCP server on stdio
tsystemc version
runwith no--timerunssc_mainto completion, and the exit code issc_main's return value. With--time Tit stops after T, prints every signal (or the--tracelist), and with--vcdtraces them.- Arguments after
--becomesc_main'sargv.
7.1 Scripts and the prompt
The commands work the same in scripts, in -c, at the prompt and through MCP:
| Command | Effect |
|---|---|
open <model> [args] | Load (compile if needed) and elaborate: runs sc_main until its first sc_start(). |
run [T] | Advance simulated time by T (e.g. 100 ns). With no T, run until no activity remains. |
continue | Let sc_main resume until its next sc_start(). |
finish | Let sc_main run to completion. |
get <name>... | Print signal, port, clock, power model or power domain values. |
set <name> <value> | Write a signal (visible next delta), set a power state, or switch a power domain. |
expect <name> <value> | Compare; prints PASS or FAIL. Failures count towards the exit code. |
power [name...] | Power report, or individual power models and domains. |
list [signals|ports|modules|processes|power|all] | List objects with their kinds. |
trace <file.vcd> [names|*] | Trace signals from now on. |
time, delta, status | Simulation time, delta count, session status. |
output, capture on|off | Model console output. |
echo, close, quit | — |
Commands are separated by newlines or ;. A # starts a comment.
Example regression script:
open counter.cpp
run 100 ns
expect count 0
set reset 0
run 50 ns
expect count 5
power
finish
7.2 How hosting works
open runs sc_main until it calls sc_start() for the first time, which is the point where elaboration is complete. sc_main then parks inside that call, and the host steps the kernel with run.
continue lets the parked sc_start run for its own duration and returns at the next sc_start. finish lets sc_main run to its end.
A model written specifically for hosting can call sc_start() once with no duration and leave all stepping to the host.
8. Embedding: the C host API
tsystemc.dll exports a plain C API, declared in include/sysc_api.h. Link with lib/libtsystemc_api.a, or load the DLL with LoadLibrary.
#include <sysc_api.h>
int h = sysc_open("model.cpp", ""); /* .cpp: compiled on demand and cached */
if (h <= 0) { char e[512]; sysc_last_error(0, e, sizeof e); puts(e); return 1; }
sysc_set(h, "din", "5");
sysc_run(h, "10 ns");
char v[64];
sysc_get(h, "sum", v, sizeof v);
int n = sysc_power_report(h, NULL, 0); /* length; then call again with a buffer */
sysc_close(h);
| Function | Purpose |
|---|---|
sysc_version | Version string. |
sysc_compile, sysc_build_exe | Build a model DLL or an executable. |
sysc_open, sysc_close | Load and elaborate a model (.cpp or DLL); release it. |
sysc_run, sysc_continue, sysc_finish | Advance time; resume sc_main. |
sysc_get, sysc_set | Read and write objects by hierarchical name. |
sysc_time, sysc_time_seconds, sysc_delta_count, sysc_status | Inspect the simulation. |
sysc_list | List objects by kind. |
sysc_trace | VCD-trace signals. |
sysc_power_report | The power report. |
sysc_capture, sysc_output | Capture the model's console output. |
sysc_exit_code, sysc_last_error | Results and errors. |
String results. Functions that return text return the full length and write as much as fits into your buffer, always NUL-terminated. Call once with a NULL buffer to size it.
Errors. Errors are negative:
| Code | Meaning |
|---|---|
SYSC_ERR | General error |
SYSC_ERR_HANDLE | Invalid handle |
SYSC_ERR_NOT_FOUND | No object with that name |
SYSC_ERR_FINISHED | sc_main has already returned |
sysc_last_error explains the most recent error.
Several models. Several models can be open at once, each with its own handle and its own copy of the engine. A model DLL stays mapped until the host process exits, because the statically linked runtime registers thread-exit hooks inside it. Reopening a model loads a fresh copy.
9. Co-simulation and AI agents
9.1 Verilog co-simulation through DPI-C
sysc_api.h also declares a DPI-friendly subset that takes only int, longint and string arguments:
sysc_dpi_open,sysc_dpi_run,sysc_dpi_run_ps,sysc_dpi_close;sysc_dpi_get_int/_str,sysc_dpi_set_int/_str;sysc_dpi_time_ps,sysc_dpi_last_error.
A SystemVerilog test bench imports them and steps the SystemC model in lock-step with the RTL:
import "DPI-C" function int sysc_dpi_open(string model, string args);
import "DPI-C" function int sysc_dpi_run_ps(int h, longint ps);
import "DPI-C" function longint sysc_dpi_get_int(int h, string name);
import "DPI-C" function int sysc_dpi_set_int(int h, string name, longint value);
Point the simulator's DPI library list at bin\tsystemc.dll. For trsim, set TUWASIM_DPI_LIBS=<install>\bin\tsystemc.dll. examples/cosim holds a complete test bench.
9.2 MCP server
tsystemc mcp speaks the Model Context Protocol (JSON-RPC 2.0 over stdio). An AI agent, or any MCP client, can then open a model, run it, inspect and change signals and power states, and read the power report. The tools are:
open,run,continue,finish,close;get,set,expect;time,status,list,trace,output;power;script, which runs any command sequence.
Model console output never mixes with the protocol stream. It is captured and returned in the tool results.
Example client configuration:
{ "mcpServers": { "tsystemc": { "command": "C:\\tools\\tsystemc-1.0.0\\bin\\tsystemc.exe", "args": ["mcp"] } } }
ICPP connects the same way:
h = icpp_mcp_connect("C:/tools/tsystemc-1.0.0/bin/tsystemc.exe mcp");
10. Examples
Every example builds with the commands in chapter 3 and runs from its own directory. Each one has a .golden file with its expected output.
| Example | Shows |
|---|---|
counter | Signals, a clocked method, reset, VCD tracing. Also used by the CLI scripts. |
tlm_lt | Loosely-timed platform: CPU with quantum keeper, router, RAM/ROM, DMI, debug transport, error responses. |
tlm_at | Approximately-timed base protocol: four phases, all return paths, memory manager, PEQs, back-pressure, an extended phase. |
tlm_bus | Two CPUs on a shared bus: multi-passthrough sockets, arbitration, address decoding, analysis-port monitor, extensions, a UART. |
power_states | A core with run/idle/sleep, transitions, auto-sleep and auto-wake, a gated dvfs_clock, VCD power trace, report. |
power_dvfs | A DVFS governor against a fixed-frequency core on the same workload: operating points, scaling, CSV trace, energy comparison. |
power_tlm_soc | A power-aware LT SoC: CPU with DVFS and sleep, bus, DRAM with self-refresh, power-gated accelerator, charge(), power_extension. |
cosim | A model driven from a SystemVerilog test bench (DPI-C) and from ICPP (MCP). |
scripts | tsystemc scripts: counter.scs, power.scs. |
To run one:
cd examples\power_dvfs
tsystemc run power_dvfs.cpp
To check one against its expected output:
g++ -std=c++17 -I..\..\include power_dvfs.cpp ..\..\lib\libsysc.a -static -o power_dvfs.exe
power_dvfs.exe > out.txt
fc out.txt power_dvfs.golden
11. Conformance notes
TSystemC implements IEEE Std 1666-2023 clauses 4 to 17. That covers the kernel, the predefined channels, the data types including fixed point, utilities, TLM-2.0 including the utilities, and TLM-1.
Where the standard and the widely used Accellera reference implementation behave differently, TSystemC follows the standard. The differences you are most likely to notice:
to_string()of integers uses the standard defaultSC_DECwith prefix ("0d5") when no representation is given.- Vector
to_int()onsc_bv/sc_lvis unsigned, as the standard specifies. - The quantum keeper's
need_sync()uses a strict>comparison. - Unnamed events. Two unnamed
sc_events in one module both get the name "event", and the clash produces a warning (§5.10.4). Name your events. - Elaboration-only calls. Calls that the standard allows only during elaboration (creating modules or ports,
sensitive <<,reset_signal_isoutside a constructor, and so on) are reported as errors rather than silently accepted.
sc_power is an extension. Its names are in namespace sc_power, and it does not change the behaviour of any standard class.
12. Troubleshooting
tsystemc run says "compile failed". The message includes the exact compiler command. Common causes:
- g++ not found. g++ is not on the
PATH; setTSYSTEMC_CXX. - Mixed runtimes. The g++ uses a different C runtime (UCRT rather than MSVCRT). Use TDM-GCC 10.3.0.
- Error in the model. The model itself does not compile. Try
g++directly as in chapter 3.
"SCE library not found". tsystemc.dll cannot find lib\libsysc.a. Keep the package layout, or set TSYSTEMC_HOME.
Warnings "event already exists ... renamed to event_0". Two unnamed events share a module. Give them names (§4.8).
A thread process crashes with deep recursion or large local arrays. Increase its stack with set_stack_size() in the constructor, or globally with SCE_STACK_SIZE.
SC_POWER/off warnings. A model called consume() while its domain was off. Either the model should check powered() first, or the domain is switched off too early.
The power CSV is empty. Call power_monitor::sample() before sc_start(). The file is complete once the simulation ends (sc_stop(), or sc_main returning).
A changed model still behaves the old way under tsystemc run. The cache key is the file path, size and modification time. Touch the file, or delete %TEMP%\tsystemc_cache.
13. Power API reference
All in namespace sc_power. Units: V, W, J, s.
power_domain (an sc_object, kind "sc_power_domain")
| Member | |
|---|---|
power_domain(name, nominal_voltage = 1.0, nominal_period = SC_ZERO_TIME) | A top-level domain. A zero period means no clock. |
power_domain(name, parent, nominal_voltage, nominal_period = SC_ZERO_TIME) | A child domain: off whenever the parent is off. |
add_operating_point(name, volts, period) | Add or redefine an operating point; chainable. The nominal point is "nominal". |
set_operating_point(name) | DVFS: switch voltage and clock period. |
operating_point_name(), operating_points() | Current point; all points. |
set_voltage(v), set_clock_period(p) | Direct control (point becomes "custom"). |
voltage(), nominal_voltage(), clock_period(), nominal_period(), frequency() | Current and nominal values; frequency in Hz (0 without a clock). |
cycles(n) | n × current clock period. |
power_off(), power_on(), is_on() | Power gating; is_on() is false if any ancestor is off. |
set_switch_energy(on_J, off_J), switch_count(), switching_energy() | Switching costs. |
energy(), power() | Totals over its models, child domains and switching. |
models(), children(), parent() | Structure. |
change_event() | Notified (delta) on every change. |
Host value: on 900 mV 600 MHz op=normal power=1.2 mW energy=3.4 uJ. Values that can be set: on, off, or an operating point name.
power_model (an sc_object, kind "sc_power_model")
| Member | |
|---|---|
power_model(name, domain) / power_model(name, &domain) / power_model(name) | With or without a domain. |
add_state(name, static_W, dynamic_W = 0) | Chainable. The first state added is the initial state. Without states there is one state "on" drawing nothing. |
add_transition(from, to, latency, J = 0) | Chainable. |
add_operation(name, J, J_per_byte = 0) | Chainable. |
set_initial_state(s) | Chainable. |
set_wake_state(s) | Auto-wake target for consume(). |
set_idle_policy(timeout, sleep, from = wake state) | Auto-sleep. |
set_state(s) → sc_time | Change state now, charge the transition energy, return its latency. |
enter_state(s) | set_state, then wait(latency). Threads only. |
state(), has_state(s), state_names(), state_event() | State queries; state_event() is notified on each change. |
consume(op, bytes = 0, count = 1) → sc_time | Charge an operation; returns the wake latency. |
operation_energy(op, bytes = 0) | Energy one call would cost now (scaled). |
add_energy(J) | Book arbitrary energy (unscaled). |
powered() | False while the domain (or an ancestor) is off. |
energy(), static_energy(), dynamic_energy(), transition_energy() | Energy up to now. |
power(), average_power() | Background power now; energy ÷ elapsed time. |
time_in_state(s), operation_count(op), transition_count() | Statistics. |
power_variable() | A const double& of the background power, for sc_trace. |
Host value: state=run power=20 mW avg=24.08 mW energy=602 nJ. The value that can be set is a state name.
dvfs_clock (an sc_signal<bool>, kind "sc_clock")
| Member | |
|---|---|
dvfs_clock(name, domain, duty = 0.5) | Follows the domain: its period, and stops while it is off. |
dvfs_clock(name, period, duty = 0.5) | Free-standing. |
set_period(p), period() | Takes effect at the next rising edge. |
enable(), disable(), enabled() | Clock gating. Stops low after the current cycle; restarts in the next delta. |
cycle_count() | Rising edges so far. |
Bind it like any clock: port(clk), sensitive << clk.posedge_event().
power_monitor (singleton)
| Member | |
|---|---|
instance() | The monitor. |
models(), domains(), find_model(name) | Registry. |
total_energy(), total_power() | Whole system, switching energy included. |
report(os), report() | The report, to a stream or as a string. |
report_at_end(on = true) | Print the report to stdout at the end of simulation. |
sample(period, csv_file) | Sampled average power to CSV. Call before sc_start. |
trace(tf) | Sampled power of each model and the total into a VCD. Needs sample(). |
TLM helpers (<sc_power_tlm.h>)
| Member | |
|---|---|
bool charge(model, trans, delay) | Charge "read"/"write"/"access" with the data length, add the wake latency to delay, and fill a power_extension if one is present. While the domain is off: TLM_GENERIC_ERROR_RESPONSE, returns false. |
std::string operation_for(model, trans) | The operation charge() would use ("" if none). |
struct power_extension | energy (J), wake_latency, charges; reset(). |
Free functions
| Function | |
|---|---|
format_si(value, unit, digits = 4) | "1.25 mW", "3.4 uJ". |
sc_trace(tf, model, name) | Trace a model's background power as a VCD real. |