Chapter 14: Synthesis Subset

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:

  1. 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.
  2. 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 the while(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 the SC_CTHREAD immediately, 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 explicit async_ and sync_ 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:

  1. When rst_async_n drops to false, the async_reset_thread immediately aborts its current execution in the while(true) loop and jumps back to the very top of the function, driving data_out_async to 0. It does not wait for clk.
  2. When rst_sync jumps to true, the sync_reset_thread behaves similarly, but it waits until the next rising edge of clk.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:

  1. Only Ports/Signals Allowed: The argument passed to async_reset_signal_is must be an sc_in<bool> or an sc_signal<bool>. You cannot use a local boolean variable or a complex datatype.

  2. Single Reset: A thread can generally only have one primary reset signal registered via these macros.

  3. 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: Declare int count; outside the loop, and write count = 0; before the first wait().

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