Writer Policies and Resolved Signals
Single-writer checks, many-writer policies, resolved logic, and when sc_signal_rv is the right model.
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.
Writer Policies and Resolved Signals
Signals look simple until two processes try to drive the same one. The IEEE 1666 LRM strictly regulates how concurrent writes to signals are handled via writer policies and resolved signal types.
The rule of thumb is simple: if the real hardware has one driver, model it with a single writer policy. If the real hardware has a resolved bus (tri-state, open-drain), model resolution intentionally using resolved signals.
Under the Hood: C++ Implementation in Accellera SystemC
How does the SystemC kernel efficiently check if multiple processes are driving a signal, and how does resolution logic work?
sc_writer_policySpecialization: Thesc_signal<T, POL>class is a template. IfPOLisSC_ONE_WRITER(the default), thewrite()method caches ansc_process_b*pointer to the current process the first time it is called in a delta cycle. On subsequent writes, it simply compares the cached pointer against the active process. If they differ, it throws anSC_REPORT_ERROR. IfPOLisSC_MANY_WRITERS, this check is optimized out entirely by the compiler via template specialization.sc_signal_resolvedLogic Tables: A resolved signal (sc_signal_resolved) does not just store a single value. It maintains astd::vector<sc_logic>representing the driving value of every connected process. During the kernel's update phase, theupdate()method iterates over this vector and applies a 2D lookup table defined in the LRM to resolve competing drives (e.g., '1' and 'Z' resolves to '1').
Source and LRM Trail
Advanced core behavior should always be checked against Docs/LRMs/SystemC_LRM_1666-2023.pdf before source details. For implementation, read .codex-src/systemc/src/sysc/kernel and .codex-src/systemc/src/sysc/communication, especially the scheduler, events, object hierarchy, writer policy, report handler, and async update path.
Writer Policies (sc_writer_policy)
The sc_core::sc_signal<T, POL> class template takes a second argument: the writer policy. The LRM defines the sc_core::sc_writer_policy enum with the following rules:
SC_ONE_WRITER(Default): The kernel tracks which process writes to the signal. If a second process attempts to write to the signal during simulation, the kernel instantly throws anSC_REPORT_ERROR. This prevents accidental multi-driver bugs that cause non-deterministic behavior depending on process scheduling order.SC_MANY_WRITERS: Allows multiple processes to write to the signal in the same delta cycle. However, the last process to write "wins" and overwrites the others. This is non-deterministic unless you explicitly control process execution order!SC_UNCHECKED_WRITERS: Disables writer checking entirely (used for performance optimization in legacy models, highly discouraged).
When to use SC_MANY_WRITERS?
Almost never. If a signal has many writers in real hardware, it is usually a multiplexer, an arbiter, or a resolved bus. Use proper structural modeling for muxes/arbiters.
Resolved Logic (sc_signal_resolved and sc_signal_rv)
For true hardware bus resolution (tri-state logic), SystemC provides four-state logic data types (sc_core::sc_logic and sc_core::sc_lv<W>) containing '0', '1', 'Z' (high-impedance), and 'X' (unknown).
When multiple processes drive an sc_core::sc_signal_resolved (or sc_core::sc_signal_rv for vectors), the kernel applies a strict resolution table during the update phase:
- Driving 'Z' and '1' resolves to '1'.
- Driving '0' and '1' resolves to 'X' (short circuit!).
- Driving 'Z' and 'Z' resolves to 'Z'.
Complete Example: Single vs. Many vs. Resolved Writers
Here is a complete sc_main example demonstrates the default single-writer failure, how SC_MANY_WRITERS works, and how `sc_signal_resolved properly models a tri-state bus.
#include <systemc>
#include <iostream>
SC_MODULE(BusDemo) {
// 1. Default: SC_ONE_WRITER (Will error if driven by multiple processes)
sc_core::sc_signal<bool> single_driver_sig{"single_driver_sig"};
// 2. SC_MANY_WRITERS: Last writer wins (Dangerous, non-deterministic)
sc_core::sc_signal<bool, sc_core::SC_MANY_WRITERS> many_driver_sig{"many_driver_sig"};
// 3. Resolved signal: Hardware-accurate tri-state bus
sc_core::sc_signal_resolved tri_state_bus{"tri_state_bus"};
SC_CTOR(BusDemo) {
SC_METHOD(driver_a);
SC_METHOD(driver_b);
SC_METHOD(monitor);
sensitive << single_driver_sig << many_driver_sig << tri_state_bus;
dont_initialize();
}
void driver_a() {
// Drive values
many_driver_sig.write(true);
tri_state_bus.write(sc_core::SC_LOGIC_1); // Drive HIGH
// Uncommenting this line and the one in driver_b causes a runtime kernel error!
// single_driver_sig.write(true);
}
void driver_b() {
// Drive competing values
many_driver_sig.write(false);
tri_state_bus.write(sc_core::SC_LOGIC_Z); // Drive High-Impedance (Z)
// single_driver_sig.write(false);
}
void monitor() {
std::cout << "@ " << sc_core::sc_time_stamp() << "\n"
<< " many_driver_sig : " << many_driver_sig.read() << " (Last writer won)\n"
<< " tri_state_bus : " << tri_state_bus.read() << " (1 and Z resolved to 1)\n";
}
};
int sc_main(int argc, char* argv[]) {
BusDemo demo("demo");
std::cout << "Starting simulation...\n";
sc_core::sc_start(10, sc_core::SC_NS);
return 0;
}Explanation of the Execution
When run, the output shows:
Starting simulation...
@ 0 s
many_driver_sig : 0 (Last writer won)
tri_state_bus : 1 (1 and Z resolved to 1)
In many_driver_sig, driver_b executed after driver_a (due to kernel scheduling), so it overwrote true with false. This is a classic race condition.
In tri_state_bus, regardless of scheduling order, the kernel's update phase resolved SC_LOGIC_1 and SC_LOGIC_Z correctly to SC_LOGIC_1, perfectly modeling a hardware bus where a driver asserts '1' while another driver disconnects ('Z').
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