Chapter 11: Advanced Core Semantics

Custom Hierarchical Channels

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

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.

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.

Comments and Corrections