// Reference

SystemC Handbook

A condensed reference for VLSI and embedded engineers — not a tutorial, not academic docs. The stuff you actually need to remember, with the gotchas nobody else documents.

Modules & Ports SC_THREAD vs SC_METHOD Events & Timing Signals & Channels Data Types Simulation Lifecycle 10 Gotchas
01

Modules & Ports

full article →

SC_MODULE skeleton

SC_MODULE(MyModule) {
  // Ports
  sc_in<bool>    clk;
  sc_in<uint32_t> data_in;
  sc_out<uint32_t> data_out;

  // Sub-modules (if any)
  SubModule sub;

  // Constructor
  SC_CTOR(MyModule) : sub("sub") {
    // Bind sub-module ports
    sub.clk(clk);

    // Register processes
    SC_THREAD(run);
    sensitive << clk.pos();
  }

  void run();
};

SC_CTOR vs SC_HAS_PROCESS

MacroUse whenConstructor signature
SC_CTOR(Name)Simple modules, no template paramsName(sc_module_name nm)
SC_HAS_PROCESS(Name)Template modules, custom constructor argsYou define it manually

Port types

PortDirectionKey methods
sc_in<T>Input.read(), .value_changed_event(), .posedge_event()
sc_out<T>Output.write(v), .read()
sc_inout<T>Bidirectional.read(), .write(v)
sc_port<IF>Custom interfaceCalls any method defined in IF

Port binding

// Direct binding — child.port(signal)
child.clk(clk_sig);
child.data(data_sig);

// Hierarchical binding — parent port maps to child port
child.clk(this->clk);   // passes parent's port down

sc_port<IF> vs sc_export<IF>

sc_port<IF>sc_export<IF>
MeaningRequired interface — "I need access to a channel"Provided interface — "I expose my channel outward"
Who holds the channelOutside (parent or sibling)Inside this module
Binding directionParent binds child's port inward to a channelParent accesses child through export, child owns the channel
Typical useCalling a service provided by another moduleWrapping an internal channel so others can call into you
// ── sc_port — "I need something from outside" ─────────
struct IRead {
  virtual int  read()  = 0;
};

SC_MODULE(Consumer) {
  sc_port<IRead> in_port;   // requires an IRead channel from outside
  SC_CTOR(Consumer) { SC_THREAD(run); }
  void run() { int v = in_port->read(); }
};

// ── sc_export — "I provide something to the outside" ──
SC_MODULE(Provider) {
  sc_export<IRead> out_export;  // advertises IRead to parent
  MyFifo            fifo;        // internal channel implementing IRead
  SC_CTOR(Provider) {
    out_export(fifo);  // bind export → internal channel
  }
};

// ── wiring in parent ─────────────────────────────────
SC_MODULE(Top) {
  Consumer consumer{"c"};
  Provider provider{"p"};
  SC_CTOR(Top) {
    consumer.in_port(provider.out_export);  // port → export, no extra signal
  }
};
Rule of thumb if a module calls into a channel → use sc_port. If a module owns the channel and lets callers in → use sc_export. Port = plug. Export = socket.

Full deep-dive → sc_port & sc_export article

02

Processes

full article →

SC_THREAD vs SC_METHOD

PropertySC_THREADSC_METHOD
Stack✅ Has stack — can suspend mid-execution❌ No stack — runs to completion
Can call wait()✅ Yes❌ Runtime error
Re-triggerResumes after wait()Re-runs from the top every time
ModelsSequential behaviour, testbenches, protocolsCombinational logic, always-sensitive processes
SensitivityStatic or dynamic via wait()Static or dynamic via next_trigger()
RTL equivalentalways @(posedge clk) with state machineCombinational always @(*)

wait() — SC_THREAD only

CallSuspends until
wait()Next event in static sensitivity list
wait(e)Event e fires
wait(e1 | e2)Either event fires (OR)
wait(e1 & e2)Both events fire (AND)
wait(10, SC_NS)10 ns of simulated time
wait(10, SC_NS, e)10 ns OR event e — whichever first
wait(SC_ZERO_TIME)One delta cycle
wait(n)n triggers of static sensitivity

next_trigger() — SC_METHOD only

CallRe-triggers when
next_trigger()Reverts to static sensitivity
next_trigger(e)Event e fires (overrides static)
next_trigger(10, SC_NS)After 10 ns
next_trigger(10, SC_NS, e)10 ns OR event e

dont_initialize()

⚠ Common Trap Without dont_initialize(), SC_METHOD and SC_THREAD fire once at time zero before any events. For clock-sensitive processes this causes a spurious execution with undefined inputs.
SC_METHOD(compute);
sensitive << clk.pos();
dont_initialize();  // ← only fire on actual clock edges
03

Events & Time

full article →

sc_event notification types

CallTypeWhen processes wake
e.notify()ImmediateCurrent delta — processes in same evaluate phase can't catch it
e.notify(SC_ZERO_TIME)DeltaNext delta cycle, same simulation time
e.notify(10, SC_NS)TimedAfter 10 ns of simulated time
e.cancel()Cancels pending delta or timed notification

sc_time units

UnitConstantExample
FemtosecondSC_FSsc_time(100, SC_FS)
PicosecondSC_PSsc_time(1, SC_PS)
NanosecondSC_NSsc_time(10, SC_NS)
MicrosecondSC_USsc_time(1, SC_US)
MillisecondSC_MSsc_time(1, SC_MS)
SecondSC_SECsc_time(1, SC_SEC)
sc_time t1(10, SC_NS);
sc_time t2 = sc_time_stamp();   // current simulation time
sc_dt::uint64 d = sc_delta_count(); // delta count at current time

Delta cycles explained

💡 Key rule A delta cycle is a zero-time evaluation pass. sc_signal::write() schedules an update for the next delta. The written value is not readable until one delta later — this is the read-lag.
→ Delta Cycles deep dive
04

Signals & Channels

full article →

sc_signal<T>

Method / EventDescription
.read()Read current value (old value until update phase)
.write(v)Schedule update (visible next delta)
.value_changed_event()Fires on any value change
.posedge_event()Fires on 0→1 transition (bool/sc_logic only)
.negedge_event()Fires on 1→0 transition
.posedge()Returns true if last transition was 0→1
⚠ Multiple drivers Default sc_signal only allows one writer. Use sc_signal<T, SC_MANY_WRITERS> for resolved signals (wired-OR/AND logic).

sc_fifo<T>

MethodBehaviour
.read()Blocking read — suspends if empty
.write(v)Blocking write — suspends if full
.nb_read(v)Non-blocking — returns false if empty
.nb_write(v)Non-blocking — returns false if full
.num_available()Items ready to read
.num_free()Space available to write
.data_read_event()Fires when an item is consumed
.data_written_event()Fires when an item is produced
→ sc_fifo deep dive

sc_mutex & sc_semaphore

Methodsc_mutexsc_semaphore
Acquire.lock() — blocking.wait() — blocking
Try acquire.trylock() — returns -1 if busy.trywait() — returns -1 if zero
Release.unlock().post()
Query.get_value()
→ sc_mutex & sc_semaphore deep dive
05

Data Types

full article →

Integer types

TypeWidthSignedUse
sc_uint<N>1–64 bitsNoGeneral purpose, RTL modeling
sc_int<N>1–64 bitsYesSigned arithmetic
sc_biguint<N>>64 bitsNoWide buses, crypto
sc_bigint<N>>64 bitsYesWide signed values
sc_uint<8> byte = 0xFF;
sc_uint<4> nibble = byte.range(7, 4);  // bits 7 downto 4
byte[3] = 1;                               // bit access

Logic types

TypeValuesUse
sc_logic'0', '1', 'X', 'Z'Single 4-state bit — tri-state, unknown
sc_lv<N>4-state per bitBus with high-impedance or unknown values
sc_logic a = SC_LOGIC_1;
sc_lv<8> bus = "10XZZZ01";
sc_logic bit = bus[3];  // SC_LOGIC_Z

Fixed-point types

TypeParametersMeaning
sc_fixed<W,I>W = total bits, I = integer bitsSigned fixed point
sc_ufixed<W,I>W = total bits, I = integer bitsUnsigned fixed point
// 16-bit total, 8 integer bits → 8 fractional bits
sc_fixed<16, 8> fval = 3.14;
06

Simulation Lifecycle

full article →

sc_main structure

int sc_main(int argc, char* argv[]) {
  // ── Elaboration ──────────────────────────────
  sc_clock clk("clk", 10, SC_NS);      // 100 MHz
  sc_signal<uint32_t> data;

  MyModule dut("dut");
  dut.clk(clk);
  dut.data(data);

  // ── Simulation ───────────────────────────────
  sc_start(100, SC_NS);   // run for 100 ns
  sc_start();             // run until sc_stop() or no more events

  return 0;
}
→ sc_main deep dive

Phase callbacks (in order)

CallbackWhenTypical use
before_end_of_elaboration()After all constructors, before port binding checkDynamic port creation, late binding
end_of_elaboration()After all ports bound, before simulationValidation, printing hierarchy
start_of_simulation()After elaboration, before first deltaInitial state setup, file opens
end_of_simulation()After sc_stop()Stats reporting, file close

sc_start() variants

CallBehaviour
sc_start()Run until sc_stop() called or no events remain
sc_start(T, unit)Run for exactly T units of simulated time
sc_start(SC_ZERO_TIME)Run one delta cycle only
sc_stop()Stop simulation from inside a process
sc_is_running()Returns true while simulation is active
→ sc_clock deep dive
07

Common Gotchas

Ten things that silently produce wrong results or hard-to-debug runtime errors.

  • #01

    Signal read-lag

    sig.write(v) schedules the update — sig.read() still returns the old value until the next delta. Writing and immediately reading in the same process gives you stale data.

  • #02

    Forgetting dont_initialize()

    Every SC_METHOD and SC_THREAD fires once at t=0 before any events. On clock-sensitive processes this is a spurious execution with undefined port values. Always add dont_initialize() after registering clock-sensitive processes.

  • #03

    SC_METHOD calling wait()

    SC_METHOD has no stack. Calling wait() inside an SC_METHOD is a runtime error: "wait() is not allowed in SC_METHOD processes". Use SC_THREAD if you need to suspend.

  • #04

    Multiple drivers on sc_signal

    Default sc_signal<T> allows only one writer. Two processes writing the same signal gives: "sc_signal: multiple drivers" at runtime. Use sc_signal<T, SC_MANY_WRITERS> or restructure to a single writer.

  • #05

    Unbound port at elaboration

    Every sc_port must be bound before sc_start(). An unbound port causes an elaboration error. Check your constructor — sub-module ports must be explicitly connected.

  • #06

    Immediate notify() in update phase

    e.notify() (immediate) called from inside a channel's update() method is illegal — you are already inside the update phase. Use e.notify(SC_ZERO_TIME) (delta notification) instead.

  • #07

    Delta cycle limit exceeded

    Combinational loops — where A writes B and B writes A — produce infinite delta cycles at the same timestep. SystemC kills the simulation with "delta limit of N exceeded". Fix by breaking the combinational loop.

  • #08

    SC_CTOR with template modules

    SC_CTOR cannot be used with template modules — it expands to a constructor with only sc_module_name. Use SC_HAS_PROCESS(Name) and define your own constructor when the module is templated.

  • #09

    sc_start() after sc_stop()

    Once sc_stop() is called, you cannot restart simulation with another sc_start(). The simulation is permanently stopped. Design your testbench to call sc_stop() only when completely done.

  • #10

    Writing to sc_out from wrong process

    Only one process should drive a given output port. Driving an sc_out from two separate SC_METHODs or SC_THREADs hits the multiple-driver rule. Consolidate writes into a single driver process.

TLM 2.0 section — coming soon

The next phase of this handbook will cover transaction-level modeling in full detail.

Initiator & Target Sockets b_transport vs nb_transport Generic Payload Direct Memory Interface (DMI) Temporal Decoupling AT vs LT Modeling

Subscribe below to get notified when it's published.

Want to go deeper than a reference?

SystemC Studio walks you through every concept hands-on — theory, code, and AI-checked exercises. Track 01 is free.

Open Studio →

📬 Get notified when TLM 2.0 is added

Plus new articles on SystemC, C++, and embedded systems.