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