LRM Bridge: Predefined Channels
sc_signal, sc_buffer, resolved signals, sc_clock, sc_fifo, sc_mutex, and sc_semaphore as standard communication building blocks.
How to Read This Lesson
This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.
The IEEE 1666 LRM provides a set of predefined primitive and hierarchical channels that form the standard toolbox for modeling hardware communication. By looking into the Accellera SystemC kernel, we can understand exactly how these channels manage their state and interface with the discrete-event scheduler.
Source and LRM Trail
This chapter is the LRM bridge. The primary reference is Docs/LRMs/SystemC_LRM_1666-2023.pdf; the secondary reference is .codex-src/systemc. Read the LRM first for the rule, then read the source to understand why the rule produces the behavior you see in a debugger.
End-to-End Predefined Channels Example
Here is a complete example demonstrates how sc_signal, sc_clock, sc_fifo, and sc_mutex interoperate to model a multi-producer, clocked FIFO system.
#include <systemc>
SC_MODULE(HardwareSystem) {
// 1. Clock and Signals
sc_core::sc_in<bool> clk;
sc_core::sc_signal<bool> data_ready{"data_ready"};
// 2. FIFO (Hierarchical Channel)
// A thread-safe queue holding up to 4 integers
sc_core::sc_fifo<int> data_fifo{"data_fifo", 4};
// 3. Mutex (Primitive Channel)
// Ensures only one producer accesses the debug port at a time
sc_core::sc_mutex bus_mutex{"bus_mutex"};
SC_CTOR(HardwareSystem) {
SC_THREAD(producer_a);
sensitive << clk.pos();
SC_THREAD(producer_b);
sensitive << clk.pos();
SC_THREAD(consumer);
sensitive << clk.pos();
}
void producer_a() {
wait(2, sc_core::SC_NS); // Wait for initialization
while(true) {
wait(); // Wait for clock
// Lock the mutex before printing to the shared bus/console
bus_mutex.lock();
std::cout << "@" << sc_core::sc_time_stamp() << " [Prod A] Acquired bus lock." << std::endl;
// Blocking write. If FIFO is full, thread suspends here until space is available.
data_fifo.write(0xA);
data_ready.write(true); // Signal an update (Delta Cycle delay)
bus_mutex.unlock();
wait(20, sc_core::SC_NS); // Work delay
}
}
void producer_b() {
wait(5, sc_core::SC_NS);
while(true) {
wait();
bus_mutex.lock();
std::cout << "@" << sc_core::sc_time_stamp() << " [Prod B] Acquired bus lock." << std::endl;
// Non-blocking write. If full, it fails gracefully.
if (data_fifo.nb_write(0xB)) {
data_ready.write(true);
}
bus_mutex.unlock();
wait(15, sc_core::SC_NS);
}
}
void consumer() {
while(true) {
wait();
// Blocking read. If FIFO is empty, thread suspends here.
int val = data_fifo.read();
std::cout << "@" << sc_core::sc_time_stamp() << " [Consumer] Read value: 0x"
<< std::hex << val << std::endl;
data_ready.write(false);
}
}
};
int sc_main(int argc, char* argv[]) {
sc_core::sc_clock clk("clk", 10, sc_core::SC_NS);
HardwareSystem sys("sys");
sys.clk(clk);
sc_core::sc_start(100, sc_core::SC_NS);
return 0;
}Channel Semantics & Kernel Implementation
sc_signal and sc_buffer
sc_signal<T> applies written values during the update phase.
Under the Hood: In sc_signal<T>::update(), the kernel checks if( !(m_new_val == m_cur_val) ). If they differ, m_cur_val = m_new_val and m_value_changed_event.notify() is called.
sc_buffer<T> inherits from sc_signal<T> but completely overrides the update() method to remove the equality check. It assigns m_cur_val = m_new_val and unconditionally calls m_value_changed_event.notify(). Use sc_buffer when the act of writing is semantically important, even if the data didn't change.
Resolved Signals (sc_rv / sc_logic)
SystemC provides resolved signals for multi-writer logic modeling (e.g., tri-state buses, wired-OR).
Under the Hood: A normal sc_signal throws SC_ERROR_MULTI_WRITE if two processes write to it in the same delta cycle. sc_signal_resolved intercepts multiple writes into an array. During the update() phase, it passes the array through a resolution table matrix defined in sc_logic_resolution() to determine the final electrical state (e.g., driving Z and 1 resolves to 1).
sc_clock
sc_clock provides periodic boolean-like toggling behavior.
Under the Hood: It is not a magical language construct. sc_clock is simply an sc_module that automatically spawns a hidden SC_METHOD. This method toggles the internal sc_signal value and uses next_trigger(m_period / 2) to wake up. This guarantees zero-skew edge events across the entire design.
sc_fifo
sc_fifo<T> models queued producer-consumer communication safely across process boundaries.
Under the Hood: It is an sc_prim_channel that uses a dynamically allocated circular buffer array (m_buf). The blocking write() method contains a while(m_num_written >= m_size) loop that calls wait(m_data_read_event). When a reader pops a value, it triggers m_data_read_event.notify(), waking up the suspended producer thread.
sc_mutex and sc_semaphore
Mutexes and semaphores control shared resource access at an abstract level.
Under the Hood: sc_mutex holds an internal sc_process_b* m_owner. If a process calls lock() and m_owner is already set to another process, it calls wait(m_free_event). When the owner calls unlock(), it sets m_owner = 0 and calls m_free_event.notify(), allowing the scheduler to wake up the blocked process.
Deep Dive: Accellera Source for sc_signal and update()
The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.
The write() Implementation
When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:
template<class T>
inline void sc_signal<T>::write(const T& value_) {
if( !(m_new_val == value_) ) {
m_new_val = value_;
this->request_update(); // Inherited from sc_prim_channel
}
}The request_update() call appends the channel to sc_simcontext::m_update_list.
The update() Phase
After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:
template<class T>
inline void sc_signal<T>::update() {
if( !(m_new_val == m_cur_val) ) {
m_cur_val = m_new_val;
m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
}
}This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.
Comments and Corrections