You know how to declare an SC_MODULE and write processes inside it. But a module sitting alone does nothing useful — it needs to exchange signals with the outside world. In SystemC, that connection point is a port, and the act of wiring it to a channel is called binding.

This article covers the three port types (sc_in, sc_out, sc_inout), how to bind them directly in sc_main, how hierarchical binding works inside a parent module, the four rules the simulator enforces, and the error messages you will see when any of those rules are broken.

1. What a Port Actually Is

A port is not a signal. It is an access point — a typed handle that a module uses to read from or write to a channel that lives outside it. The module declares the port; the channel is created and bound to it elsewhere.

The analogy in hardware is exact: a chip pin is a port. The PCB trace connecting two pins is the channel (sc_signal). The chip body is the module. You never solder a signal wire inside the chip; you solder it to a pin on the outside. SystemC enforces the same separation.

// The channel lives outside the module
sc_signal<bool> clk_wire;   // like a PCB trace

// The port lives inside the module
SC_MODULE(Adder) {
    sc_in<bool> clk;           // like a chip pin — reads from whatever is bound
    // ...
};

// Binding connects the pin to the trace
Adder adder("adder");
adder.clk(clk_wire);          // solder the pin to the trace

2. The Three Port Types

SystemC provides three port templates for signal-level communication. All three are thin wrappers around the underlying channel interface — they delegate every operation to the bound channel.

Port type Direction Operations available Hardware analogy
sc_in<T> Input read(), events Input pin
sc_out<T> Output write() Output pin (tri-state off)
sc_inout<T> Bidirectional read(), write(), events Bidirectional / open-drain pin
SC_MODULE(Alu) {
    // inputs — read-only inside this module
    sc_in<sc_uint<8>>  op_a;
    sc_in<sc_uint<8>>  op_b;
    sc_in<sc_uint<3>>  opcode;
    sc_in<bool>         clk;
    sc_in<bool>         rst_n;

    // outputs — write-only inside this module
    sc_out<sc_uint<8>> result;
    sc_out<bool>        overflow;

    // bidirectional — read and write inside this module
    sc_inout<sc_uint<8>> bus_data;

    SC_CTOR(Alu) { SC_METHOD(compute); sensitive << clk.pos(); }
    void compute();
};
Ports enforce direction at the interface level, not at runtime. Calling write() on an sc_in port is a compile error — the method simply does not exist in the interface. This catches direction violations at compile time rather than silently producing wrong simulation results.

3. Direct Binding in sc_main

The most common pattern: modules are instantiated in sc_main, signals are declared there, and ports are bound to signals before sc_start() is called. Binding uses the function-call operator — port(channel).

int sc_main(int, char**) {

    // ── Channels (live in sc_main) ────────────────────────
    sc_signal<sc_uint<8>> a_sig, b_sig, result_sig;
    sc_signal<sc_uint<3>> opcode_sig;
    sc_signal<bool>        clk_sig, rst_sig, ovf_sig;

    // ── Module instances ──────────────────────────────────
    Alu alu("alu");

    // ── Binding: port(channel) ───────────────────────────
    alu.op_a(a_sig);
    alu.op_b(b_sig);
    alu.opcode(opcode_sig);
    alu.clk(clk_sig);
    alu.rst_n(rst_sig);
    alu.result(result_sig);
    alu.overflow(ovf_sig);

    // All ports bound — safe to start
    sc_start(100, SC_NS);
    return 0;
}

The binding call alu.op_a(a_sig) is syntactic sugar for alu.op_a.bind(a_sig). Both forms are valid; the function-call style is more common.

All ports must be bound before sc_start(). SystemC checks every port during the end-of-elaboration phase — just before simulation begins. Any unbound port produces a fatal error and halts the simulation. There is no default "disconnected" state.

4. Hierarchical Binding — Forwarding Ports to Children

When one module contains another (a hierarchy), the outer module's own ports need to be forwarded to the inner module's ports. This is called hierarchical binding and happens inside the outer module's constructor.

// Inner module
SC_MODULE(Counter) {
    sc_in<bool>          clk;
    sc_in<bool>          rst;
    sc_out<sc_uint<8>>  count;

    SC_CTOR(Counter) {
        SC_METHOD(tick); sensitive << clk.pos();
    }
    void tick() {
        if (rst.read()) count.write(0);
        else          count.write(count.read() + 1);
    }
};

// Outer module — wraps Counter and exposes the same pins
SC_MODULE(Top) {
    // Top's own ports — these are what sc_main binds to
    sc_in<bool>          clk;
    sc_in<bool>          rst;
    sc_out<sc_uint<8>>  count_out;

    Counter ctr;  // child module — must be a member, not a pointer

    SC_CTOR(Top) : ctr("ctr") {
        // Hierarchical binding: forward Top's ports to ctr's ports
        ctr.clk(clk);           // Top's sc_in  → Counter's sc_in
        ctr.rst(rst);           // Top's sc_in  → Counter's sc_in
        ctr.count(count_out);   // Counter's sc_out → Top's sc_out
    }
};

In sc_main, only Top's ports are visible and need binding — Counter's ports are fully wired internally:

int sc_main(int, char**) {
    sc_signal<bool>        clk_sig, rst_sig;
    sc_signal<sc_uint<8>> count_sig;

    Top top("top");
    top.clk(clk_sig);
    top.rst(rst_sig);
    top.count_out(count_sig);

    sc_start(200, SC_NS);
    return 0;
}
Port-to-port binding is only legal between parent and child. You cannot bind two sibling ports directly to each other — mod_a.out(mod_b.in) is illegal. Sibling modules must communicate through a shared channel (sc_signal). Only a parent forwarding its own port to a child's port is allowed.

5. The Four Port Binding Rules

The SystemC LRM defines four rules that every binding must satisfy. Violating any of them causes a fatal error, either at compile time or at the end-of-elaboration check before simulation starts.

#RuleWhat happens if broken
1 Every port must be bound exactly once before simulation starts Runtime fatal: "port is not bound"
2 A port can only be bound once — duplicate binding is illegal Runtime fatal: "port is already bound"
3 Port-to-port binding is only legal between a parent's port and a direct child's port Runtime fatal: "bind between incompatible ports"
4 The channel bound must implement the interface the port requires Compile error — type mismatch
// Rule 1 violation — unbound port
Alu alu("alu");
alu.op_a(a_sig);
// forgot alu.op_b — fatal at sc_start()

// Rule 2 violation — double binding
alu.clk(clk_sig);
alu.clk(clk_sig);  // fatal: port already bound

// Rule 3 violation — sibling-to-sibling port binding
ModA a("a"); ModB b("b");
a.out(b.in);  // fatal: a and b are siblings, not parent-child
              // correct: sc_signal<T> wire; a.out(wire); b.in(wire);

// Rule 4 violation — type mismatch (compile error)
sc_signal<int>  int_sig;
sc_in<bool>     bool_port;
bool_port(int_sig);  // compile error: sc_signal<int> does not satisfy sc_signal_in_if<bool>

6. Port Events — posedge, negedge, value_changed

sc_in and sc_inout expose the same event interface as the bound sc_signal — the port delegates all event calls through to the channel. This means you can use a port directly in a sensitivity list without naming the underlying signal.

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

    SC_CTOR(FlipFlop) {
        // Static sensitivity via port events
        SC_METHOD(capture); sensitive << clk.pos();   // rising edge only
        SC_METHOD(sample);  sensitive << d;            // any d change
    }

    void capture() { q.write(d.read()); }
    void sample()  { /* combinational logic */ }
};

// Dynamic sensitivity in SC_THREAD
void monitor_thread() {
    while (true) {
        wait(clk.posedge_event());      // wait for rising edge
        wait(d.value_changed_event());   // wait for d to change
        wait(clk.negedge_event());       // wait for falling edge
    }
}
Port event methodAvailable onFires when
pos()sc_in<bool>, sc_inout<bool>Shorthand for posedge_event() in sensitive list
neg()sc_in<bool>, sc_inout<bool>Shorthand for negedge_event() in sensitive list
posedge_event()sc_in<bool>, sc_inout<bool>0 → 1 transition on the bound signal
negedge_event()sc_in<bool>, sc_inout<bool>1 → 0 transition on the bound signal
value_changed_event()sc_in<T>, sc_inout<T>Any value change on the bound signal
default_event()sc_in<T>, sc_inout<T>Alias for value_changed_event()

7. Complete Example — Clock Divider with Testbench

A divide-by-2 clock divider with synchronous reset, a testbench that drives it, and the top-level that wires them together using both direct and hierarchical binding patterns.

#include <systemc.h>
#include <iostream>
using namespace std;

// ── Clock Divider ─────────────────────────────────────────────
SC_MODULE(ClkDiv2) {
    sc_in<bool>  clk_in;
    sc_in<bool>  rst;
    sc_out<bool> clk_out;

    SC_CTOR(ClkDiv2) {
        SC_METHOD(divide); sensitive << clk_in.pos();
    }

    void divide() {
        if (rst.read()) clk_out.write(false);
        else           clk_out.write(!clk_out.read());
    }
};

// ── Testbench ─────────────────────────────────────────────────
SC_MODULE(TB) {
    sc_in<bool>  clk_div;   // observe divided clock
    sc_out<bool> clk;       // drive the source clock
    sc_out<bool> rst;       // drive reset

    SC_CTOR(TB) { SC_THREAD(run); }

    void run() {
        rst.write(true); clk.write(false);
        wait(15, SC_NS);
        rst.write(false);

        for (int i = 0; i < 12; i++) {
            clk.write(!clk.read());
            wait(5, SC_NS);
            cout << "t=" << sc_time_stamp()
                 << "  clk=" << clk.read()
                 << "  div=" << clk_div.read() << endl;
        }
        sc_stop();
    }
};

// ── Top — wires DUT and TB together ──────────────────────────
SC_MODULE(Top) {
    ClkDiv2 dut;
    TB      tb;

    sc_signal<bool> clk_sig, rst_sig, div_sig;

    SC_CTOR(Top) : dut("dut"), tb("tb") {
        // Direct binding — all channels live in Top
        dut.clk_in(clk_sig);
        dut.rst(rst_sig);
        dut.clk_out(div_sig);

        tb.clk(clk_sig);
        tb.rst(rst_sig);
        tb.clk_div(div_sig);
    }
};

int sc_main(int, char**) {
    Top top("top");  // all binding happens inside Top's ctor
    sc_start();
    return 0;
}

Expected output:

t=20 ns   clk=1  div=0
t=25 ns   clk=0  div=0
t=30 ns   clk=1  div=1
t=35 ns   clk=0  div=1
t=40 ns   clk=1  div=0
t=45 ns   clk=0  div=0
t=50 ns   clk=1  div=1
...

8. Common Binding Errors and What They Mean

These are the exact error strings SystemC prints. Each maps to a specific rule violation — knowing them saves significant debugging time.

Error messageCauseFix
port is not bound A port was never bound before sc_start() Add the missing module.port(channel) call
port is already bound bind() called twice on the same port Remove the duplicate binding
sc_port_base::bind: cannot bind ... to sc_port Sibling port-to-port binding attempted Introduce an sc_signal between them
Compile error: no matching call to bind Channel type doesn't satisfy port's interface Match the template type — e.g. sc_in<bool> needs sc_signal<bool>
port not connected in simulation output Process reads a port that isn't driving meaningful data yet Ensure the driving module has written before the reading module runs

Summary


📬 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.

Part of the SystemC Foundations guide Browse all SystemC guides →