Chapter 8: SystemC AMS

Solver Synchronization and Execution Semantics

Learn how the SystemC AMS solvers synchronize the TDF, LSF, and ELN models of computation with each other and the discrete-event kernel.

How to Read This Lesson

AMS becomes easier once you separate continuous-time intent from discrete-event synchronization. Watch where the analog cluster meets the SystemC kernel.

Solver Synchronization and Execution Semantics

Because SystemC AMS allows you to mix and match Timed Data Flow (TDF), Linear Signal Flow (LSF), Electrical Linear Networks (ELN), and standard SystemC discrete-event (DE) models, it relies on a sophisticated synchronization layer to ensure time remains consistent across all domains.

In this tutorial, we will explore how these different solvers interact, how time steps are propagated, and how data moves securely between the analog and digital worlds.

Source and LRM Trail

For AMS, use Docs/LRMs/SystemC_AMS_2_0_LRM.pdf as the standard reference. The implementation/source trail is the AMS proof-of-concept code and examples where available, plus the SystemC DE boundary in .codex-src/systemc/src/sysc/kernel. Pay special attention to TDF rate, delay, timestep, converter ports, and solver synchronization.

TDF as the Master Scheduler

In SystemC AMS, the TDF solver acts as the primary time-keeper for the continuous-time domains.

When you build an LSF or ELN model, those models form a system of continuous-time equations. However, a computer cannot simulate continuous time infinitely; it must discretize it. The LSF and ELN solvers derive their calculation timesteps directly from the TDF cluster they are connected to.

If you connect an ELN circuit to a TDF module that runs with a 1.0 us timestep, the ELN solver will simulate that circuit in chunks of 1.0 us to provide a synchronized output back to the TDF module.

Synchronizing with the SystemC DE Kernel

Ultimately, your AMS system will likely need to communicate with a standard SystemC discrete-event digital component (like a processor, an interrupt controller, or a TLM bus).

Synchronization between the AMS solvers and the SystemC DE kernel is done exclusively through specialized converter ports:

  • sca_tdf::sca_de::sca_in<T>: Reads a standard sc_core::sc_signal<T> from the DE kernel into the TDF domain.
  • sca_tdf::sca_de::sca_out<T>: Writes a TDF sample to a standard sc_core::sc_signal<T> in the DE kernel.

The Synchronization Rules

According to the IEEE 1666.1 LRM:

  1. Reading from DE to TDF: When the TDF solver reads a value from a sca_de::sca_in port during a processing() callback, it reads the value that was present on the SystemC signal at the first delta cycle of the current SystemC simulation time. The value is assumed to remain constant for the duration of the TDF timestep.
  2. Writing from TDF to DE: When the TDF solver writes a sample to a sca_de::sca_out port, the value is written to the SystemC signal at the exact corresponding SystemC time, triggering standard SystemC update phases and events (like value_changed_event()).

Under the Hood: The Converter Ports and the Update Phase

How does sca_tdf::sca_de::sca_out safely inject continuous mathematical data into a discrete digital event queue without causing race conditions?

Inside the Accellera implementation, sca_de::sca_out contains a pointer to the bound sc_core::sc_signal. During the TDF processing() phase, calling analog_out.write(voltage) does not immediately trigger any SystemC events. Instead, the AMS solver buffers the requested data alongside its calculated timestamp.

When the TDF cluster finishes executing its static schedule for the current cluster period, the internal SC_THREAD managing that cluster actively calls m_bound_sc_signal->write(value). In SystemC, .write() merely places an update request into the DE kernel's evaluation queue. The AMS cluster thread then explicitly calls sc_core::wait() to suspend itself. This action hands control back to the SystemC DE kernel, which immediately runs the Update Phase. The SystemC kernel safely resolves the sc_signal writes, triggers the value_changed_event(), and wakes up any purely digital modules (like the DigitalMonitor below) in the subsequent delta cycle. This carefully orchestrated thread yield guarantees thread-safety and causality between the analog equations and digital logic.

Complete Example: DE and TDF Synchronization

Here is a complete, compilable example demonstrates a TDF module generating a mathematical waveform, connected via a converter port to a purely digital SystemC module (a simple threshold monitor) that wakes up only when the analog value crosses a boundary.

#include <systemc>
#include <systemc-ams.h>
 
// 1. TDF Domain: Analog Waveform Generator
SCA_TDF_MODULE(AnalogSensor) {
    // A converter port: TDF driving a standard SystemC DE signal
    sca_tdf::sca_de::sca_out<double> analog_out;
 
    SCA_CTOR(AnalogSensor) {}
 
    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // 1 millisecond sampling
    }
 
    void processing() {
        // Generate a slow 1 Hz sine wave
        double t = get_time().to_seconds();
        double voltage = 5.0 * std::sin(2.0 * M_PI * 1.0 * t);
        
        // Write to the DE kernel. This schedules a SystemC event.
        analog_out.write(voltage);
    }
};
 
// 2. Digital Domain: Discrete-Event Monitor
SC_MODULE(DigitalMonitor) {
    sc_core::sc_in<double> analog_in;
 
    SC_CTOR(DigitalMonitor) {
        SC_THREAD(monitor_thread);
        // Wake up whenever the analog signal changes
        sensitive << analog_in.value_changed_event(); 
        dont_initialize();
    }
 
    void monitor_thread() {
        bool threshold_exceeded = false;
 
        while (true) {
            double current_val = analog_in.read();
            
            if (current_val > 4.5 && !threshold_exceeded) {
                threshold_exceeded = true;
                std::cout << "@ " << sc_core::sc_time_stamp() 
                          << " [DIGITAL ALARM]: Voltage exceeded 4.5V! (Value: " 
                          << current_val << "V)\n";
            } 
            else if (current_val < 4.0 && threshold_exceeded) {
                threshold_exceeded = false;
                std::cout << "@ " << sc_core::sc_time_stamp() 
                          << " [DIGITAL CLEAR]: Voltage dropped below 4.0V.\n";
            }
 
            wait(); // Wait for the next value_changed_event
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    // 3. The boundary signal: Standard SystemC sc_signal
    sc_core::sc_signal<double> sig_analog_voltage("sig_analog_voltage");
 
    // 4. Instantiate and bind
    AnalogSensor sensor("sensor");
    sensor.analog_out(sig_analog_voltage); // TDF writes to DE
 
    DigitalMonitor monitor("monitor");
    monitor.analog_in(sig_analog_voltage); // DE reads from DE
 
    // Setup Tracing to observe the synchronization
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("sync_wave");
    sca_util::sca_trace(tf, sig_analog_voltage, "Sensor_Voltage");
 
    // Run simulation for 2 seconds
    std::cout << "Starting mixed-signal simulation...\n";
    sc_core::sc_start(2.0, sc_core::SC_SEC);
    
    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}

Event-Driven TDF Activation (Dynamic TDF)

In Dynamic TDF (introduced in SystemC AMS 2.0), a TDF module can actually suspend its static schedule and wait for a discrete-event trigger.

By using request_next_activation(port.default_event()) inside the processing() callback, a TDF module can wake up reactively when a digital signal changes. This saves immense computation power when the analog domain is otherwise idle, rather than forcing the TDF solver to calculate empty samples on a fixed timestep.

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