SC_MODULE is the fundamental building block of every SystemC design. It is the C++ equivalent of a VHDL entity or a Verilog module — a self-contained unit that combines state, behaviour, and connectivity into one object. Understanding how it works at the C++ level, how its constructor wires things together, and how the kernel invokes its processes is the foundation for everything else in SystemC.

This article unpacks the module from the macro expansion all the way through ports, processes, sensitivity lists, and multi-level hierarchy — finishing with a complete three-module example you can compile and run.

What Is SC_MODULE?

SC_MODULE(Name) is a C preprocessor macro that expands to a struct (publicly inherited from sc_core::sc_module) and a static type-name method. Using struct instead of class means all members are public by default — a deliberate choice to keep port and process declarations visible to parent modules.

/* What you write */
SC_MODULE(Counter) { ... };

/* What the preprocessor generates */
struct Counter : sc_core::sc_module {
  typedef Counter SC_CURRENT_USER_MODULE;
  /* ... your declarations go here ... */
};

The SC_CURRENT_USER_MODULE typedef is consumed by SC_CTOR and the process-registration macros (SC_METHOD, SC_THREAD, SC_CTHREAD) so they know which class they belong to at compile time.

Key point SC_MODULE is not magic — it is a thin macro layer over standard C++. Every rule of C++ class design applies: constructors, destructors, inheritance, templates. If you can explain the expanded struct, you understand the module.

SC_CTOR and SC_HAS_PROCESS

The module constructor is where the kernel gets told about your processes, their sensitivity lists, and any sub-module port bindings. Two macros handle this:

SC_CTOR — the simple constructor

SC_CTOR(ModuleName) expands to a constructor that accepts only a const sc_module_name & argument. Use it when your module needs no extra configuration parameters.

SC_MODULE(Adder) {
  sc_in<int>  a, b;
  sc_out<int> sum;

  SC_CTOR(Adder) {
    SC_METHOD(compute);
    sensitive << a << b;
  }

  void compute() { sum.write(a.read() + b.read()); }
};

SC_HAS_PROCESS — the flexible constructor

When you need extra constructor arguments (e.g., a configurable data width or a memory size), SC_CTOR won't work. Use SC_HAS_PROCESS(ModuleName) inside the class body to register the process infrastructure, then write a normal C++ constructor.

SC_MODULE(Memory) {
  sc_in<bool>         clk;
  sc_in<sc_uint<16>> addr;
  sc_out<sc_uint<8>> data_out;

  SC_HAS_PROCESS(Memory);   /* enables SC_METHOD/SC_THREAD macros */

  explicit Memory(sc_module_name nm, unsigned size_kb)
    : sc_module(nm), m_size(size_kb * 1024)
  {
    m_mem.resize(m_size, 0);
    SC_METHOD(read_mem);
    sensitive << clk.pos();
  }

  void read_mem() {
    data_out.write(m_mem[addr.read()]);
  }

private:
  unsigned           m_size;
  std::vector<uint8_t> m_mem;
};
Rule of thumb Default parameter → use SC_CTOR. Extra parameters → use SC_HAS_PROCESS + a normal constructor that calls sc_module(nm) explicitly.

Ports — The Module Interface

Ports are typed connection points declared as member variables. They are bound to sc_signal objects (or other channels) during elaboration — never during simulation.

Port typeDirectionread()write()Typical use
sc_in<T>InputClock, data inputs, control signals
sc_out<T>OutputResult, status outputs
sc_inout<T>BidirectionalBidirectional buses, shared lines
sc_in_rv<N>Resolved inputMulti-driver resolved logic bus

Ports are bound in the parent scope using the function-call syntax module_instance.port(signal), or with named binding module_instance.port.bind(signal):

/* in sc_main or a parent module's constructor */
sc_signal<bool> clk_sig, en_sig, q_sig;

Toggle toggle_inst("toggle");
toggle_inst.clk(clk_sig);    /* positional binding */
toggle_inst.en(en_sig);
toggle_inst.q(q_sig);
Common mistake Binding ports inside a process (after sc_start() has been called) is illegal. Port binding must complete before elaboration ends — i.e., before sc_start().

SC_METHOD — Combinational and Edge-Triggered Logic

SC_METHOD registers a member function as a method process. The kernel calls it every time an event on its sensitivity list occurs. The function runs to completion — it cannot call wait(), and it must return before the kernel can schedule anything else.

This makes it the natural choice for:

SC_MODULE(DFF) {
  sc_in<bool>  clk, d;
  sc_out<bool> q;

  SC_CTOR(DFF) {
    SC_METHOD(on_clk);
    sensitive << clk.pos();   /* rising-edge only */
    q.initialize(false);
  }

  void on_clk() {
    q.write(d.read());        /* capture D on rising clock edge */
  }
};

dont_initialize()

By default, every method process runs once at the start of simulation (time 0) to establish initial output values. Call dont_initialize() in the constructor right after the sensitivity declaration to suppress this:

SC_CTOR(DFF) {
  SC_METHOD(on_clk);
  sensitive << clk.pos();
  dont_initialize();   /* do NOT run on_clk at time 0 */
}

SC_THREAD — Sequential Processes with wait()

SC_THREAD registers a member function as a thread process. Unlike SC_METHOD, a thread can suspend itself by calling wait(). It runs exactly once from its entry point — so an infinite loop with wait() inside is the idiomatic pattern for modelling hardware that operates continuously over time.

Typical uses:

SC_MODULE(ClockGen) {
  sc_out<bool> clk;

  SC_CTOR(ClockGen) {
    SC_THREAD(gen_clk);
    /* no static sensitivity — thread uses wait() for timing */
    clk.initialize(false);
  }

  void gen_clk() {
    while (true) {
      clk.write(false);
      wait(5, SC_NS);     /* low half-period */
      clk.write(true);
      wait(5, SC_NS);     /* high half-period  → 10 ns period, 100 MHz */
    }
  }
};
Deadlock trap A thread that never calls wait() — even once — will spin forever. The simulator hangs, consuming 100% CPU with no simulation time advancing. Always ensure every code path through a thread loop eventually hits a wait().

Sensitivity Lists — Static and Dynamic

A sensitivity list tells the kernel which events should wake a process. SystemC supports two kinds:

Static sensitivity

Declared in the constructor using the sensitive object. Fixed for the lifetime of the simulation — cannot be changed after elaboration ends.

SC_CTOR(Mux2) {
  SC_METHOD(select);
  sensitive << a << b << sel;   /* any change on a, b, or sel */
}

/* Edge modifiers */
sensitive << clk.pos();    /* rising edge */
sensitive << clk.neg();    /* falling edge */
sensitive << clk;          /* both edges (value change event) */

Dynamic sensitivity

Used inside SC_THREAD (and SC_CTHREAD). The wait() call accepts an event, a list of events, or a time duration — making the sensitivity dynamic per-invocation.

void run() {
  while (true) {
    wait(clk.posedge_event());          /* wait for next rising edge */

    wait(10, SC_NS);                    /* wait 10 nanoseconds */

    wait(done_event | timeout_event);   /* wait for either event */
  }
}
wait() formSuspends until…
wait()Any event on the static sensitivity list
wait(event)The specified sc_event fires
wait(e1 | e2)Either event fires (OR)
wait(e1 & e2)Both events fire (AND — same delta)
wait(N, SC_NS)N nanoseconds of simulation time pass
wait(N, SC_NS, event)Event fires OR N ns elapses (timeout)

Module Hierarchy — Composing the Design

Real designs nest modules inside other modules. The parent creates sub-module instances, declares the signals that connect them, and binds ports in its constructor. This is identical to how you instantiate components in VHDL or modules in Verilog.

SC_MODULE(Top) {
  /* ── Sub-module instances ── */
  ClockGen  clkgen;
  Counter   counter;

  /* ── Internal signals ── */
  sc_signal<bool>        clk_sig;
  sc_signal<sc_uint<8>> count_sig;

  SC_CTOR(Top)
    : clkgen("clkgen"), counter("counter")  /* pass name strings */
  {
    /* ── Port binding ── */
    clkgen.clk(clk_sig);

    counter.clk(clk_sig);
    counter.count(count_sig);
  }
};

Key rules for hierarchy:

sc_main — The Three Stages

Every SystemC program's entry point is sc_main(). It is not main() — the SystemC library provides its own main() that calls sc_main() after internal initialisation.

int sc_main(int argc, char* argv[]) {

  /* ── 1. Elaboration ────────────────────────────────────────── */
  /* Instantiate modules, declare signals, bind ports.            */
  /* All constructors run here. The kernel is NOT yet simulating. */

  sc_signal<bool>        clk, en;
  sc_signal<sc_uint<8>> count;

  ClockGen  clkgen ("clkgen");
  Counter   counter("counter");

  clkgen.clk(clk);
  counter.clk(clk);
  counter.en(en);
  counter.count(count);

  en.write(true);

  /* ── 2. Simulation ─────────────────────────────────────────── */
  /* sc_start() hands control to the kernel scheduler.            */
  /* Processes execute in response to events until time runs out  */
  /* or sc_stop() is called from inside a process.                */

  sc_start(200, SC_NS);

  /* ── 3. Post-processing ────────────────────────────────────── */
  /* sc_start() has returned. Read results, check pass/fail.      */
  /* Module destructors have NOT yet run — signals are still valid.*/

  std::cout << "Final count: " << count.read() << std::endl;

  return 0;
}
Why this order matters Anything you do to a signal or port after sc_start() returns is post-simulation read-only inspection. Writing to signals in stage 3 does nothing useful — the kernel is no longer running. Use stage 3 only for checking results and printing reports.

Complete Example — ClockGen, Counter, and Testbench

Here is a self-contained three-file design that demonstrates every concept covered: a clock generator, an enable-gated 8-bit counter, and a testbench that enables the counter, runs it, then disables it mid-simulation.

/* ════════════════════════════════════════════════════════════════
   File: clock_gen.h
   ════════════════════════════════════════════════════════════════ */
#include <systemc.h>

SC_MODULE(ClockGen) {
  sc_out<bool> clk;

  SC_CTOR(ClockGen) {
    SC_THREAD(gen);
    clk.initialize(false);
  }

  void gen() {
    while (true) {
      wait(5, SC_NS);
      clk.write(!clk.read());
    }
  }
};

/* ════════════════════════════════════════════════════════════════
   File: counter.h
   ════════════════════════════════════════════════════════════════ */
SC_MODULE(Counter) {
  sc_in<bool>          clk;
  sc_in<bool>          en;     /* count only when high */
  sc_out<sc_uint<8>>  count;

  SC_CTOR(Counter) {
    SC_METHOD(on_clk);
    sensitive << clk.pos();
    dont_initialize();
    count.initialize(0);
  }

  void on_clk() {
    if (en.read())
      count.write(count.read() + 1);
  }
};

/* ════════════════════════════════════════════════════════════════
   File: tb.h  — testbench
   ════════════════════════════════════════════════════════════════ */
SC_MODULE(Testbench) {
  sc_in<sc_uint<8>>  count;
  sc_out<bool>        en;

  SC_CTOR(Testbench) {
    SC_THREAD(run);
  }

  void run() {
    en.write(false);
    wait(15, SC_NS);

    std::cout << "[TB] Enabling counter at " << sc_time_stamp() << std::endl;
    en.write(true);
    wait(80, SC_NS);       /* let it count for 8 clock cycles */

    std::cout << "[TB] Count after 8 cycles: " << count.read() << std::endl;

    en.write(false);
    wait(20, SC_NS);

    std::cout << "[TB] Count frozen at: " << count.read() << std::endl;
    sc_stop();
  }
};

/* ════════════════════════════════════════════════════════════════
   File: main.cpp
   ════════════════════════════════════════════════════════════════ */
int sc_main(int, char**) {
  sc_signal<bool>        clk_sig, en_sig;
  sc_signal<sc_uint<8>> count_sig;

  ClockGen  clkgen ("clkgen");
  Counter   counter("counter");
  Testbench tb     ("tb");

  clkgen.clk(clk_sig);

  counter.clk(clk_sig);
  counter.en(en_sig);
  counter.count(count_sig);

  tb.count(count_sig);
  tb.en(en_sig);

  sc_start();   /* run until sc_stop() */
  return 0;
}

Expected output (approximate — clock edges at 5 ns intervals):

[TB] Enabling counter at 15 ns
[TB] Count after 8 cycles: 8
[TB] Count frozen at: 8

SC_MODULE Quick Reference

ConstructWhat it doesWhen to use
SC_MODULE(Name)Declares a struct inheriting sc_moduleEvery module
SC_CTOR(Name)Standard constructor (name arg only)No extra ctor params
SC_HAS_PROCESS(Name)Enables process macros for custom ctorExtra ctor params needed
SC_METHOD(fn)Registers a method processCombinational / edge-triggered logic
SC_THREAD(fn)Registers a thread processSequential behaviour, testbenches
sensitive << portStatic sensitivity (any value change)Combinational inputs
sensitive << port.pos()Static sensitivity (rising edge)Clock-triggered method
dont_initialize()Skip time-0 method invocationRegisters (avoid spurious output)
wait(N, SC_NS)Suspend thread for N nanosecondsTimed delays in threads
wait(event)Suspend thread until event firesEvent-driven sequencing
sc_start(T, SC_NS)Run simulation for T nanosecondsBounded simulation runs
sc_stop()Halt simulation from inside a processTestbench end-of-test

📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.