FIFOs, Mutexes, Semaphores, and Custom Channels
When to use built-in channels and how to design communication abstractions of your own.
How to Read This Lesson
Think of this chapter as wiring discipline. Ports, exports, interfaces, and channels are not decorative; they are how the model states its contract before time starts moving.
Not every connection should be a signal. SystemC includes higher-level channels such as FIFOs, mutexes, and semaphores because many system models care more about transactions and resources than individual wires.
Source and LRM Trail
The standard contract lives in Docs/LRMs/SystemC_LRM_1666-2023.pdf around interfaces, ports, exports, primitive channels, hierarchical channels, and predefined channels. The implementation trail is .codex-src/systemc/src/sysc/communication: sc_port, sc_export, sc_interface, sc_prim_channel, sc_signal, sc_fifo, and the writer policy helpers.
sc_fifo
sc_fifo<T> models queued communication:
#include <systemc>
using namespace sc_core;
SC_MODULE(Producer) {
sc_fifo_out<int> out{"out"};
SC_CTOR(Producer) {
SC_THREAD(run);
}
void run() {
for (int i = 0; i != 8; ++i) {
out.write(i);
std::cout << "Wrote " << i << " at " << sc_time_stamp() << std::endl;
wait(10, SC_NS);
}
}
};
SC_MODULE(Consumer) {
sc_fifo_in<int> in{"in"};
SC_CTOR(Consumer) { SC_THREAD(run); }
void run() {
while (true) {
int val = in.read();
std::cout << "Read " << val << " at " << sc_time_stamp() << std::endl;
}
}
};
int sc_main(int, char*[]) {
sc_fifo<int> fifo(4);
Producer p("p");
Consumer c("c");
p.out(fifo);
c.in(fifo);
sc_start(100, SC_NS);
return 0;
}The blocking write() waits when the FIFO is full. The blocking read() waits when the FIFO is empty. This is often perfect for modeling pipelines, queues, and producer-consumer systems.
Mutexes and Semaphores
sc_mutex and sc_semaphore model shared resources. Use them when the model question is about arbitration or resource ownership.
#include <systemc>
using namespace sc_core;
SC_MODULE(Master) {
sc_port<sc_mutex_if> bus_lock{"bus_lock"};
SC_CTOR(Master) { SC_THREAD(run); }
void run() {
bus_lock->lock();
std::cout << name() << " got lock at " << sc_time_stamp() << std::endl;
wait(20, SC_NS);
bus_lock->unlock();
}
};
int sc_main(int, char*[]) {
sc_mutex mutex("mutex");
Master m1("m1"), m2("m2");
m1.bus_lock(mutex);
m2.bus_lock(mutex);
sc_start(50, SC_NS);
return 0;
}These are modeling constructs, not magic performance tools. Use them when they express the system behavior clearly.
Custom Channels
Custom channels are where SystemC becomes a modeling framework rather than a fixed simulator. You define an interface, implement it in a channel, and bind modules to that interface.
Good custom channels hide policy:
- timing
- arbitration
- buffering
- tracing
- protocol conversion
- statistics
The module using the channel should care about the operation it needs, not the machinery behind it.
Design Rule
If the connection is a wire, use a signal. If the connection is a transaction, use an interface or TLM socket. If the connection is a queue, use a FIFO. If the connection is a shared resource, use a mutex or semaphore. The model reads better when the channel matches the concept.
Under the Hood: sc_fifo and sc_mutex Blocking Semantics
sc_fifo<T> and sc_mutex implement blocking synchronization using sc_event and wait().
In sysc/communication/sc_fifo.cpp, if a thread calls read() but the FIFO is empty, the channel calls wait(m_data_written_event). The thread is suspended. When another thread calls write(), it executes m_data_written_event.notify(SC_ZERO_TIME). The blocked reader is added to the runnable queue and resumes in the next delta cycle.
Similarly, sc_mutex::lock() checks if the mutex is available. If not, it waits on m_free_event. When locked, the mutex stores the process handle (sc_get_current_process_handle()) of the owner to ensure only the owner can unlock() it, throwing an error if a different process attempts to release the lock.
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