Chapter 14: Synthesis Subset

Ports, Interfaces, and Hardware Boundaries

How HLS interprets SystemC ports and pins at the hardware boundary.

How to Read This Lesson

For synthesis, the question changes from 'can C++ run this?' to 'can hardware be built from this?' Keep storage, timing, and static structure in your head as you read.

Ports, Interfaces, and Hardware Boundaries

When you write a SystemC module for simulation, ports are just pointers to channels. However, when you pass that SystemC module to an HLS tool to generate RTL (Verilog/VHDL), those ports become physical wires and pins on a silicon chip.

Source and LRM Trail

For synthesis, use Docs/LRMs/SystemC_Synthesis_Subset_1_4_7.pdf as the primary contract and Docs/LRMs/SystemC_LRM_1666-2023.pdf for base SystemC semantics. Source internals explain simulation behavior, but synthesizability is a tool contract: focus on static structure, reset modeling, wait placement, and bounded loops.

Pin-Level Interfaces

The most basic and universally supported synthesizable ports are sc_in<T> and sc_out<T>. When an HLS tool sees sc_in<sc_uint<8>> data_in;, it generates an 8-bit wide input wire bus on the Verilog module.

Clocks and Resets

Clocks and resets must be explicitly identified. HLS tools use these to schedule the Finite State Machine (FSM).

  • sc_in_clk (or sc_in<bool>) is used for clocks.
  • You must use reset_signal_is() during the SC_CTOR to explicitly tell the HLS tool which port is the reset, and whether it is active-high or active-low.

Array of Ports and HLS

Arrays of ports are synthesizable as long as the array size is a constant, statically determinable integer. Dynamically allocated arrays of ports using sc_vector are supported by modern HLS tools only if the vector size is completely fixed and known during elaboration.

Here is a complete compilable example demonstrating static port arrays and explicit reset binding:

#include <systemc>
#include <sysc/datatypes/int/sc_uint.h>
 
using namespace sc_core;
 
SC_MODULE(PortArrayDemo) {
    sc_in_clk clk{"clk"};
    sc_in<bool> rst{"rst"};
    
    // Supported: Generates 4 distinct 8-bit input buses in RTL
    sc_in<sc_dt::sc_uint<8>> data_bus[4]; 
    sc_out<sc_dt::sc_uint<10>> sum_out{"sum_out"};
 
    SC_CTOR(PortArrayDemo) {
        SC_CTHREAD(compute_sum, clk.pos());
        reset_signal_is(rst, true);
    }
 
    void compute_sum() {
        sum_out.write(0);
        wait();
        
        while (true) {
            sc_dt::sc_uint<10> temp_sum = 0;
            // Static loops can be unrolled by HLS tools
            for (int i = 0; i < 4; ++i) {
                temp_sum += data_bus[i].read();
            }
            sum_out.write(temp_sum);
            wait();
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    sc_clock clk("clk", 10, SC_NS);
    sc_signal<bool> rst("rst");
    sc_signal<sc_dt::sc_uint<8>> bus_signals[4];
    sc_signal<sc_dt::sc_uint<10>> sum_signal("sum_signal");
 
    PortArrayDemo demo("demo");
    demo.clk(clk);
    demo.rst(rst);
    demo.sum_out(sum_signal);
    
    for (int i = 0; i < 4; ++i) {
        demo.data_bus[i](bus_signals[i]);
    }
 
    sc_start(50, SC_NS);
    return 0;
}

Custom Channels and Interfaces

In the SystemC simulation world, you can write custom hierarchical channels that implement complex interfaces.

In the Synthesis world, this is heavily restricted. Most HLS tools require all module boundaries to eventually resolve down to standard pin-level ports (sc_in, sc_out). You cannot easily pass a complex C++ object or a generic custom interface across a physical hardware boundary without explicitly defining the wire protocol.

To get around this, modern HLS tools have introduced specialized synthesis pragmas or compiler directives that instruct the tool on how to map a C++ function call into a hardware bus protocol (like AXI4).

In the next tutorial, we will discuss the holy grail of system-level design: Synthesizing TLM-2.0.

Under the Hood: HLS Port Mapping

In pure SystemC, an sc_in<T> is just an sc_port bound to an sc_signal. However, an HLS compiler maps these ports to specific hardware protocols using pragmas or attributes. For example, if you synthesize a function with sc_in<int>, the tool might map it to a simple wire. If you use sc_fifo_in<T>, the HLS tool automatically synthesizes the valid, ready, and data handshake signals of an AXI4-Stream interface. The semantics of read() on the FIFO are translated into an FSM state that halts execution until valid == 1 and asserts ready = 1.

Standard and Source Deep Dive: Port Binding

Port binding is the topological glue of a SystemC model. The IEEE 1666-2023 LRM Sections 4.2.1 (Elaboration) and Section 6.11-6.13 (Ports, Exports, Interfaces) rigidly define how structural connections are made and verified.

Inside the Accellera Source: sc_port_b and sc_port_registry

In src/sysc/communication/sc_port.h/cpp, all specialized sc_port<IF> classes derive from a non-template base class sc_port_b. When you declare sc_port<BusIf> bus{"bus"};, the constructor ultimately calls sc_simcontext::get_port_registry()->insert(this).

The sc_port_registry (located in src/sysc/kernel/sc_simcontext.cpp) is the global list of every port in the simulation.

When you write cpu.bus.bind(subsystem.target); in your C++ code, you are invoking the bind() method on sc_port. However, this does not immediately resolve the C++ pointer! Instead, the port simply stores a generic pointer to the bound object in an internal array (because a port can be bound to multiple channels if the port's N parameter is > 1).

The Elaboration Phase: complete_binding()

The real magic happens when sc_start() is called. Before simulation begins, sc_start() invokes sc_simcontext::elaborate(), which ultimately calls sc_port_registry::complete_binding().

If you trace sysc/kernel/sc_simcontext.cpp, you will see complete_binding() iterate over every single port in the design. For each port:

  1. It traverses the binding tree. If Port A is bound to Port B, and Port B is bound to Channel C, it recursively walks from A -> B -> C to find the actual sc_interface implementation.
  2. Type Checking: It uses C++ RTTI (dynamic_cast) to verify that the target object actually implements the interface required by the port.
    // Abstract representation of the kernel's check:
    sc_interface* target_if = dynamic_cast<sc_interface*>(bound_object);
    if (!target_if) { SC_REPORT_ERROR("Port binding failed: interface mismatch"); }
  3. It resolves the final interface pointer and stores it directly inside the port's m_interface pointer array.

Zero-Overhead Simulation Dispatch

Why delay pointer resolution until complete_binding()? Because once elaboration finishes, the port has an absolute, direct C++ pointer to the implementing channel.

In src/sysc/communication/sc_port.h, the overloaded operator-> is extraordinarily simple:

template <class IF>
inline IF* sc_port<IF>::operator -> () {
    return m_interface;
}

During simulation, when a thread executes bus->write(0x10, data);, there are no map lookups, no string comparisons, and no routing tables. It is exactly equivalent to a direct C++ virtual function call on the channel object.

Comments and Corrections