Processes: SC_METHOD vs SC_CTHREAD
How SystemC processes are interpreted by High-Level Synthesis tools.
How to Read This Lesson
For synthesis, the question changes from 'can C++ run this?' to 'can hardware be built from this?' Keep storage, timing, and static structure in your head as you read.
Processes: SC_METHOD vs SC_CTHREAD
In SystemC simulation, you have three process types: SC_METHOD, SC_THREAD, and SC_CTHREAD. When it comes to High-Level Synthesis (HLS), this list is strictly narrowed.
Source and LRM Trail
For synthesis, use Docs/LRMs/SystemC_Synthesis_Subset_1_4_7.pdf as the primary contract and Docs/LRMs/SystemC_LRM_1666-2023.pdf for base SystemC semantics. Source internals explain simulation behavior, but synthesizability is a tool contract: focus on static structure, reset modeling, wait placement, and bounded loops.
The Ban on SC_THREAD
The standard SC_THREAD is generally not recommended for synthesis, and many strict HLS tools outright reject it.
Why? An SC_THREAD is an unconstrained coroutine. It can call wait() on any arbitrary event, at any time, with no relationship to a clock. Hardware requires a clock to schedule state transitions. Synthesizing an unconstrained SC_THREAD into a Finite State Machine (FSM) is extremely difficult and ambiguous.
Using SC_CTHREAD and SC_METHOD
Instead of SC_THREAD, HLS tools mandate the use of SC_CTHREAD (Clocked Thread) for sequential logic, and SC_METHOD for purely combinational logic.
An SC_CTHREAD is statically bound to a single clock edge during elaboration. When you call wait() inside an SC_CTHREAD, the HLS tool knows exactly what it means: "Wait exactly one clock cycle." This allows the HLS compiler to automatically extract a Finite State Machine (FSM).
An SC_METHOD must not have any internal state (no static variables) and must write to all its outputs in every possible execution path to avoid inferring latches.
Here is a complete, fully compilable example demonstrating both synthesizable process types:
#include <systemc>
using namespace sc_core;
SC_MODULE(HLS_Process_Demo) {
sc_in_clk clk{"clk"};
sc_in<bool> reset{"reset"};
// Inputs and Outputs for Combinational Logic
sc_in<int> a{"a"};
sc_in<int> b{"b"};
sc_out<int> max_val{"max_val"};
// Output for Sequential Logic (FSM)
sc_out<int> state_out{"state_out"};
// 1. SC_CTHREAD for sequential FSM logic
void clocked_logic() {
// Reset state
state_out.write(0);
wait(); // Wait 1 clock cycle
while (true) {
// State 1
state_out.write(1);
wait(); // Wait 1 clock cycle
// State 2
state_out.write(2);
wait(); // Wait 1 clock cycle
}
}
// 2. SC_METHOD for purely combinational logic
void combinational_logic() {
// If a > b, out = a, else out = b (Combinational Mux)
if (a.read() > b.read()) {
max_val.write(a.read());
} else {
max_val.write(b.read());
}
}
SC_CTOR(HLS_Process_Demo) {
SC_CTHREAD(clocked_logic, clk.pos());
reset_signal_is(reset, true); // Synchronous reset declaration
SC_METHOD(combinational_logic);
sensitive << a << b; // Sensitive to inputs, exactly like combinational hardware
}
};
int sc_main(int argc, char* argv[]) {
sc_clock clk("clk", 10, SC_NS);
sc_signal<bool> reset("reset");
sc_signal<int> a("a"), b("b"), max_val("max_val"), state_out("state_out");
HLS_Process_Demo demo("demo");
demo.clk(clk);
demo.reset(reset);
demo.a(a);
demo.b(b);
demo.max_val(max_val);
demo.state_out(state_out);
sc_start(50, SC_NS);
return 0;
}For synthesis, SC_METHOD is sensitive to changes in its inputs, exactly as it is in simulation. By using SC_CTHREAD for sequential logic and SC_METHOD for combinational logic, you guarantee your code remains safely within the synthesizable subset.
Under the Hood: SC_CTHREAD and Clock Boundaries
While SC_THREAD is standard in SystemC, SC_CTHREAD is heavily preferred in HLS.
In sysc/kernel/sc_cthread_process.cpp, an SC_CTHREAD is explicitly tied to the edge of a clock signal (sensitive << clk.pos()).
For HLS tools, a call to wait() inside an SC_CTHREAD represents a distinct clock boundary (a state in the generated Finite State Machine). The logic between two wait() calls is synthesized as the combinational logic that computes the next state and outputs for that specific cycle.
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