Chapter 11: Advanced Core Semantics

Custom Hierarchical Channels

How to build complex protocols by inheriting from sc_channel and implementing sc_interface.

Listen to this lessonAudiobook mode

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.

  1. sc_channel: If you look at the Accellera source code, class sc_channel is literally just a typedef sc_module sc_channel;. By inheriting from sc_channel, your class inherits sc_object, participates in the sc_simcontext hierarchy, and can register SC_THREADs.
  2. sc_interface: This is a pure abstract base class with zero state. It requires the implementation of a register_port callback.
  3. 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 its sc_port<bus_if> to your ahb_bus instance, the SystemC kernel's end_of_elaboration phase checks that ahb_bus dynamically casts to bus_if and stores a pointer. When the initiator calls port->burst_write(), it dereferences that pointer and executes the C++ method within the context of the ahb_bus object's memory space.

Standard and source context

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.

Source-reading checkpoint

For a custom channel, read the Accellera sc_prim_channel path and its request_update hook. That is the implementation seam behind deferred channel updates.

Lesson self-check

Can you answer these clearly?

Keep moving when you can answer each question without looking back at the lesson.

Comments and Corrections