Custom Hierarchical Channels
How to build complex protocols by inheriting from sc_channel and implementing sc_interface.
How to Read This Lesson
These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.
Custom Hierarchical Channels
In SystemC, communication is separated from computation. Computation happens in sc_module processes, while communication is handled by channels implementing sc_interface.
While sc_signal and sc_fifo are primitive channels (they hook directly into the kernel's update phase without possessing their own processes), you can also build Hierarchical Channels.
Under the Hood: C++ Implementation in Accellera SystemC
How does a hierarchical channel combine the behavior of a module and an interface? By leveraging C++ multiple inheritance.
sc_channel: If you look at the Accellera source code,class sc_channelis literally just atypedef sc_module sc_channel;. By inheriting fromsc_channel, your class inheritssc_object, participates in thesc_simcontexthierarchy, and can registerSC_THREADs.sc_interface: This is a pure abstract base class with zero state. It requires the implementation of aregister_portcallback.- The Bridge: When you write
class ahb_bus : public sc_channel, public bus_if, you are bridging the static module hierarchy with dynamic TLM-like function calls. When an initiator module binds itssc_port<bus_if>to yourahb_businstance, the SystemC kernel'send_of_elaborationphase checks thatahb_busdynamically casts tobus_ifand stores a pointer. When the initiator callsport->burst_write(), it dereferences that pointer and executes the C++ method within the context of theahb_busobject's memory space.
Source and LRM Trail
Advanced core behavior should always be checked against Docs/LRMs/SystemC_LRM_1666-2023.pdf before source details. For implementation, read .codex-src/systemc/src/sysc/kernel and .codex-src/systemc/src/sysc/communication, especially the scheduler, events, object hierarchy, writer policy, report handler, and async update path.
What is a Hierarchical Channel?
A hierarchical channel is essentially just an sc_module (technically derived from sc_channel, which is a typedef of sc_module) that also implements one or more sc_interface classes. Because it is a module, it can contain its own ports, signals, and processes!
This allows you to encapsulate complex bus protocols (like AXI, PCIe, or I2C), arbiters, or shared memories into a single reusable block.
Complete Custom Bus Example
Below is a complete, fully compilable sc_main example demonstrating how to define an interface, implement it in a hierarchical channel equipped with a mutex for arbitration, and use it from an initiator module.
#include <systemc>
#include <iostream>
#include <vector>
// 1. Define the Interface
class bus_if : virtual public sc_core::sc_interface {
public:
virtual void burst_write(int addr, const std::vector<int>& data) = 0;
virtual void burst_read(int addr, std::vector<int>& data, int len) = 0;
};
// 2. Implement the Hierarchical Channel
class ahb_bus_channel : public sc_core::sc_channel, public bus_if {
public:
SC_HAS_PROCESS(ahb_bus_channel);
ahb_bus_channel(sc_core::sc_module_name name) : sc_core::sc_channel(name) {
// A hierarchical channel can have its own threads to manage background tasks
SC_THREAD(monitor_thread);
}
// Implement the interface write method
void burst_write(int addr, const std::vector<int>& data) override {
// Lock the bus to prevent other initiators from interfering
bus_mutex.lock();
std::cout << "@" << sc_core::sc_time_stamp() << " BUS: Burst Write starting at address "
<< addr << " for length " << data.size() << std::endl;
// Simulate bus latency based on burst length
sc_core::wait(10 * data.size(), sc_core::SC_NS);
// Store data in fake memory
for(size_t i = 0; i < data.size(); i++) {
memory[addr + i] = data[i];
}
bus_mutex.unlock();
}
// Implement the interface read method
void burst_read(int addr, std::vector<int>& data, int len) override {
bus_mutex.lock();
std::cout << "@" << sc_core::sc_time_stamp() << " BUS: Burst Read starting at address "
<< addr << " for length " << len << std::endl;
sc_core::wait(10 * len, sc_core::SC_NS);
data.clear();
for(int i = 0; i < len; i++) {
data.push_back(memory[addr + i]);
}
bus_mutex.unlock();
}
private:
sc_core::sc_mutex bus_mutex;
std::map<int, int> memory;
void monitor_thread() {
while(true) {
sc_core::wait(100, sc_core::SC_NS);
// Background monitoring logic could go here
}
}
};
// 3. Define an Initiator that uses the bus
SC_MODULE(initiator) {
// Port bound to the custom bus interface
sc_core::sc_port<bus_if> bus_port;
SC_CTOR(initiator) {
SC_THREAD(run);
}
void run() {
sc_core::wait(5, sc_core::SC_NS);
std::vector<int> write_data = {42, 43, 44};
bus_port->burst_write(0x1000, write_data);
std::vector<int> read_data;
bus_port->burst_read(0x1000, read_data, 3);
std::cout << "Initiator read back: ";
for (int v : read_data) std::cout << v << " ";
std::cout << std::endl;
}
};
// 4. Top-level integration
int sc_main(int argc, char* argv[]) {
// Instantiate the channel and the initiator
ahb_bus_channel custom_bus("custom_bus");
initiator init1("init1");
// Bind the initiator's port directly to the hierarchical channel
init1.bus_port(custom_bus);
sc_core::sc_start(200, sc_core::SC_NS);
return 0;
}By using hierarchical channels, your IP blocks only need an sc_port<bus_if>. They call port->burst_write() without knowing or caring about the complex arbitration logic, mutexes, and delays happening inside the ahb_bus_channel. This encapsulation heavily promotes reuse and abstraction.
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:
- 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_interfaceimplementation. - 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"); } - It resolves the final interface pointer and stores it directly inside the port's
m_interfacepointer 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