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
| Macro | Use when | Constructor signature |
SC_CTOR(Name) | Simple modules, no template params | Name(sc_module_name nm) |
SC_HAS_PROCESS(Name) | Template modules, custom constructor args | You define it manually |
Port types
| Port | Direction | Key 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 interface | Calls 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> |
| Meaning | Required interface — "I need access to a channel" | Provided interface — "I expose my channel outward" |
| Who holds the channel | Outside (parent or sibling) | Inside this module |
| Binding direction | Parent binds child's port inward to a channel | Parent accesses child through export, child owns the channel |
| Typical use | Calling a service provided by another module | Wrapping 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
SC_THREAD vs SC_METHOD
| Property | SC_THREAD | SC_METHOD |
| Stack | ✅ Has stack — can suspend mid-execution | ❌ No stack — runs to completion |
Can call wait() | ✅ Yes | ❌ Runtime error |
| Re-trigger | Resumes after wait() | Re-runs from the top every time |
| Models | Sequential behaviour, testbenches, protocols | Combinational logic, always-sensitive processes |
| Sensitivity | Static or dynamic via wait() | Static or dynamic via next_trigger() |
| RTL equivalent | always @(posedge clk) with state machine | Combinational always @(*) |
wait() — SC_THREAD only
| Call | Suspends 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
| Call | Re-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
sc_event notification types
| Call | Type | When processes wake |
e.notify() | Immediate | Current delta — processes in same evaluate phase can't catch it |
e.notify(SC_ZERO_TIME) | Delta | Next delta cycle, same simulation time |
e.notify(10, SC_NS) | Timed | After 10 ns of simulated time |
e.cancel() | — | Cancels pending delta or timed notification |
sc_time units
| Unit | Constant | Example |
| Femtosecond | SC_FS | sc_time(100, SC_FS) |
| Picosecond | SC_PS | sc_time(1, SC_PS) |
| Nanosecond | SC_NS | sc_time(10, SC_NS) |
| Microsecond | SC_US | sc_time(1, SC_US) |
| Millisecond | SC_MS | sc_time(1, SC_MS) |
| Second | SC_SEC | sc_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
sc_signal<T>
| Method / Event | Description |
.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>
| Method | Behaviour |
.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
| Method | sc_mutex | sc_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
Integer types
| Type | Width | Signed | Use |
sc_uint<N> | 1–64 bits | No | General purpose, RTL modeling |
sc_int<N> | 1–64 bits | Yes | Signed arithmetic |
sc_biguint<N> | >64 bits | No | Wide buses, crypto |
sc_bigint<N> | >64 bits | Yes | Wide 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
| Type | Values | Use |
sc_logic | '0', '1', 'X', 'Z' | Single 4-state bit — tri-state, unknown |
sc_lv<N> | 4-state per bit | Bus 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
| Type | Parameters | Meaning |
sc_fixed<W,I> | W = total bits, I = integer bits | Signed fixed point |
sc_ufixed<W,I> | W = total bits, I = integer bits | Unsigned fixed point |
// 16-bit total, 8 integer bits → 8 fractional bits
sc_fixed<16, 8> fval = 3.14;
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)
| Callback | When | Typical use |
before_end_of_elaboration() | After all constructors, before port binding check | Dynamic port creation, late binding |
end_of_elaboration() | After all ports bound, before simulation | Validation, printing hierarchy |
start_of_simulation() | After elaboration, before first delta | Initial state setup, file opens |
end_of_simulation() | After sc_stop() | Stats reporting, file close |
sc_start() variants
| Call | Behaviour |
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
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.