Resets in High-Level Synthesis (Synchronous vs. Asynchronous)
A deep dive into how High-Level Synthesis (HLS) models and generates synchronous and asynchronous resets from SystemC code according to the Synthesis Subset LRM.
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.
Resets in High-Level Synthesis (HLS)
In digital design, resets are critical for bringing your hardware into a known, predictable initial state. In standard C++ software, you initialize variables in a constructor. In hardware, however, state-holding elements (like flip-flops and registers) require a physical reset routing network.
When writing SystemC for High-Level Synthesis (HLS), the SystemC Synthesis Subset LRM explicitly defines how resets must be modeled so that the HLS compiler can correctly map them to physical asynchronous or synchronous reset pins in RTL (Verilog/VHDL).
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 Anatomy of an HLS Process
In HLS, hardware blocks are predominantly modeled using SC_CTHREAD (Clocked Threads) or clocked SC_METHODs.
To make a thread synthesizable with a reset, you must strictly follow a specific coding pattern:
- The Reset Block (Initialization): The code immediately following the start of the function, up to the first
wait(), is considered the reset block. This defines the default state of all variables and outputs when the reset signal is active. - The Functional Block (Infinite Loop): An infinite
while(true)loop follows the reset block. This represents the actual operational hardware logic that executes on every clock cycle when the reset is not active.
[!WARNING] If you omit the initial
wait()after your reset assignments, or if you place logic before thewhile(true)loop that takes multiple cycles, most HLS tools will reject the code or synthesize it incorrectly.
The Kernel Reality: Exception Unwinding
SystemC provides specific registration macros to tell the simulation kernel (and the HLS compiler) how a reset behaves. But how does a thread jump out of an infinite loop back to the top of its function?
The Accellera kernel implements this using C++ exceptions. When a reset condition is triggered, the kernel invokes sc_process_b::reset_process(), which throws an internal sc_unwind_exception inside your coroutine. This abruptly unwinds the call stack out of the while(true) loop, catches it in the kernel's process runner, and restarts the thread function from line 1.
async_reset_signal_is(port, active_level): The reset is asynchronous. The kernel actively monitors this signal independently of the clock. If the active level is hit, the kernel interrupts theSC_CTHREADimmediately, throws the unwind exception, and executes the reset block in the current delta cycle.sync_reset_signal_is(port, active_level): The reset is synchronous. The kernel only evaluates the reset signal when the thread wakes up due to its static sensitivity (e.g.,clk.pos()). If the reset is active, it throws the unwind exception on the clock edge.- Legacy Note: The older
reset_signal_is()macro is generally interpreted as synchronous by default, but modern IEEE 1666 standard practices prefer the explicitasync_andsync_variants for clarity.
End-to-End Example: Modeling Resets
Below is a complete, compilable SystemC model demonstrating both an active-low asynchronous reset and an active-high synchronous reset in the same module.
#include <systemc.h>
// -------------------------------------------------------------------------
// Synthesizable Hardware Module
// -------------------------------------------------------------------------
SC_MODULE(ResetDemo) {
// Inputs
sc_in<bool> clk;
sc_in<bool> rst_async_n; // Active-low asynchronous reset
sc_in<bool> rst_sync; // Active-high synchronous reset
sc_in<int> data_in;
// Outputs
sc_out<int> data_out_async;
sc_out<int> data_out_sync;
// SC_CTHREAD modeling asynchronous reset
void async_reset_thread() {
// --- RESET BLOCK ---
// This executes immediately when rst_async_n goes low.
data_out_async.write(0);
wait(); // REQUIRED: Boundary between reset and functional logic
// --- FUNCTIONAL BLOCK ---
while (true) {
// Read input, add 1, and drive output
data_out_async.write(data_in.read() + 1);
// If rst_async_n goes low while waiting here,
// the kernel throws an sc_unwind_exception and restarts the thread!
wait();
}
}
// SC_CTHREAD modeling synchronous reset
void sync_reset_thread() {
// --- RESET BLOCK ---
// This executes on the clock edge only if rst_sync is high.
data_out_sync.write(0);
wait(); // REQUIRED: Boundary between reset and functional logic
// --- FUNCTIONAL BLOCK ---
while (true) {
// Read input, add 2, and drive output
data_out_sync.write(data_in.read() + 2);
wait(); // Wait for the next rising clock edge
}
}
SC_CTOR(ResetDemo) {
// Register the asynchronous thread
SC_CTHREAD(async_reset_thread, clk.pos());
// Tell the tool: rst_async_n is an async reset, active when false (low)
async_reset_signal_is(rst_async_n, false);
// Register the synchronous thread
SC_CTHREAD(sync_reset_thread, clk.pos());
// Tell the tool: rst_sync is a sync reset, active when true (high)
sync_reset_signal_is(rst_sync, true);
}
};
// -------------------------------------------------------------------------
// Testbench / Simulation
// -------------------------------------------------------------------------
int sc_main(int argc, char* argv[]) {
// Signals to wire up the DUT
sc_clock clk("clk", 10, SC_NS);
sc_signal<bool> rst_async_n;
sc_signal<bool> rst_sync;
sc_signal<int> data_in;
sc_signal<int> data_out_async;
sc_signal<int> data_out_sync;
// Instantiate and bind
ResetDemo dut("dut");
dut.clk(clk);
dut.rst_async_n(rst_async_n);
dut.rst_sync(rst_sync);
dut.data_in(data_in);
dut.data_out_async(data_out_async);
dut.data_out_sync(data_out_sync);
// Setup waveform tracing for debugging
sc_trace_file* tf = sc_create_vcd_trace_file("reset_waveforms");
tf->set_time_unit(1, SC_NS);
sc_trace(tf, clk, "clk");
sc_trace(tf, rst_async_n, "rst_async_n");
sc_trace(tf, rst_sync, "rst_sync");
sc_trace(tf, data_in, "data_in");
sc_trace(tf, data_out_async, "data_out_async");
sc_trace(tf, data_out_sync, "data_out_sync");
// Initialization
rst_async_n.write(true); // Deassert async reset (active low)
rst_sync.write(false); // Deassert sync reset (active high)
data_in.write(10);
std::cout << "@" << sc_time_stamp() << " Starting simulation..." << std::endl;
sc_start(15, SC_NS);
// 1. Trigger Asynchronous Reset (Mid-cycle)
std::cout << "@" << sc_time_stamp() << " Asserting Async Reset (rst_async_n = 0)" << std::endl;
rst_async_n.write(false);
sc_start(10, SC_NS);
std::cout << "@" << sc_time_stamp() << " Deasserting Async Reset" << std::endl;
rst_async_n.write(true);
sc_start(15, SC_NS);
// 2. Trigger Synchronous Reset
std::cout << "@" << sc_time_stamp() << " Asserting Sync Reset (rst_sync = 1)" << std::endl;
rst_sync.write(true);
sc_start(15, SC_NS);
std::cout << "@" << sc_time_stamp() << " Deasserting Sync Reset" << std::endl;
rst_sync.write(false);
// 3. Normal Operation Change
std::cout << "@" << sc_time_stamp() << " Changing data_in to 42" << std::endl;
data_in.write(42);
sc_start(30, SC_NS);
std::cout << "@" << sc_time_stamp() << " Simulation complete." << std::endl;
sc_close_vcd_trace_file(tf);
return 0;
}Understanding the Simulation Output
When you run this code, the SystemC kernel enforces the semantics you declared using the sc_unwind_exception:
- When
rst_async_ndrops tofalse, theasync_reset_threadimmediately aborts its current execution in thewhile(true)loop and jumps back to the very top of the function, drivingdata_out_asyncto0. It does not wait forclk. - When
rst_syncjumps totrue, thesync_reset_threadbehaves similarly, but it waits until the next rising edge ofclk.pos()before throwing the unwind exception and jumping back to the top of its function.
HLS LRM Restrictions on Resets
When targeting physical silicon, the SystemC Synthesis Subset LRM enforces several strict rules regarding resets:
-
Only Ports/Signals Allowed: The argument passed to
async_reset_signal_ismust be ansc_in<bool>or ansc_signal<bool>. You cannot use a local boolean variable or a complex datatype. -
Single Reset: A thread can generally only have one primary reset signal registered via these macros.
-
No Variable Declarations with Initialization: Do not initialize local variables in their declaration if they are intended to represent hardware state holding registers. Initialize them inside the reset block explicitly.
Incorrect:
int count = 0;(HLS tools often ignore C++ initialization). Correct: Declareint count;outside the loop, and writecount = 0;before the firstwait().
By adhering strictly to these LRM guidelines, your C++ simulation will exactly match the RTL hardware generated by your HLS compiler.
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