Port Binding in SystemC — sc_in, sc_out, sc_inout and Hierarchical Binding
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(); };
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.
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; }
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.
| # | Rule | What 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 method | Available on | Fires 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 message | Cause | Fix |
|---|---|---|
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
- A port is an access point — not a signal. It delegates every operation to the channel it is bound to.
sc_in<T>is read-only;sc_out<T>is write-only;sc_inout<T>is both. Direction is enforced at compile time.- Direct binding:
module.port(channel)— happens insc_mainor a parent module's constructor. - Hierarchical binding: a parent forwards its own port to a child's port —
child.port(parent_port)— only legal inside the parent's constructor. - Sibling modules cannot bind ports to each other directly — they must share a channel.
- All four rules: every port bound exactly once, no double binding, no sibling port-to-port, type must match.
sc_in<bool>exposespos()andneg()for edge-sensitive sensitivity — the port delegates to the bound signal's events.- Any unbound port is a fatal error detected at end-of-elaboration — just before
sc_start()runs.
Part of the SystemC Foundations guide Browse all SystemC guides →