Chapter 8: SystemC AMS

AMS Converter Ports and Domain Boundaries

How SystemC discrete-event models connect to AMS TDF models through converter ports and disciplined boundary design.

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.

AMS Converter Ports and Domain Boundaries

Mixed-signal modeling is mostly about managing boundaries. A digital controller, a TLM register block, or a standard SystemC SC_THREAD inevitably needs to interact with an AMS dataflow model.

If you connect them casually, the model becomes confusing and simulation performance degrades. A clean boundary explicitly defines:

  • How often analog values are sampled.
  • How continuous/sampled analog crossings become digital discrete events.
  • Which domain owns the timing.

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.

The Converter Ports

To cross between the SystemC Discrete-Event (DE) kernel and the AMS TDF solver, the LRM mandates the use of converter ports:

  • sca_tdf::sca_de::sca_in<T>: TDF reads a DE sc_signal.
  • sca_tdf::sca_de::sca_out<T>: TDF writes to a DE sc_signal.

[!WARNING] A TDF output port driving a discrete-event signal can produce an event storm. If a TDF module running at a 1 ns timestep continuously writes slightly fluctuating analog values to a DE signal, the SystemC kernel will wake up millions of times per second, destroying your simulation performance. You should always use a comparator or threshold detector in the AMS domain to reduce the event frequency before crossing the boundary into DE.

Complete Example: Smart Temperature Sensor

The following complete sc_main example perfectly illustrates mixed-signal boundaries. It models a temperature sensor. The sensor's raw physics (a TDF waveform) is processed by a TDF Threshold Comparator. Only when the temperature crosses a dangerous threshold does the TDF module output a boolean event into the SystemC Digital domain, triggering an interrupt.

#include <systemc>
#include <systemc-ams.h>
 
// 1. TDF Domain: Analog Temperature Physics
SCA_TDF_MODULE(AnalogTempPhysics) {
    sca_tdf::sca_out<double> temp_out;
 
    SCA_CTOR(AnalogTempPhysics) {}
 
    void set_attributes() {
        set_timestep(10.0, sc_core::SC_MS); // Sample physics every 10 ms
    }
 
    void processing() {
        // Simulate a slow temperature rise (e.g., an engine heating up)
        double t = get_time().to_seconds();
        double temperature = 20.0 + (t * 5.0); // Starts at 20C, rises 5C per second
        temp_out.write(temperature);
    }
};
 
// 2. TDF Domain: Analog Threshold Comparator
// This module prevents "event storms" by only outputting boolean state changes.
SCA_TDF_MODULE(ThresholdComparator) {
    sca_tdf::sca_in<double> temp_in;
    
    // Converter Port: TDF driving standard SystemC DE
    sca_tdf::sca_de::sca_out<bool> alarm_out; 
 
    double threshold;
 
    SCA_CTOR(ThresholdComparator) : threshold(75.0) {}
 
    void set_attributes() {
        // Inherits timestep from AnalogTempPhysics (10 ms)
    }
 
    void processing() {
        double current_temp = temp_in.read();
        
        // Write the boolean evaluation. 
        // Note: Writing the exact same boolean value repeatedly to a SystemC sc_signal
        // does NOT trigger value_changed_event() continuously, saving performance.
        alarm_out.write(current_temp >= threshold);
    }
};
 
// 3. Digital Domain: SystemC Interrupt Controller
SC_MODULE(InterruptController) {
    // Standard SystemC DE port
    sc_core::sc_in<bool> hw_alarm;
 
    SC_CTOR(InterruptController) {
        SC_THREAD(monitor_interrupts);
        // Only wake up when the boolean alarm state changes
        sensitive << hw_alarm.value_changed_event();
        dont_initialize();
    }
 
    void monitor_interrupts() {
        while(true) {
            if (hw_alarm.read() == true) {
                std::cout << "@ " << sc_core::sc_time_stamp() 
                          << " [DIGITAL_CTRL]: HARDWARE ALARM TRIGGERED! Shutting down system.\n";
            } else {
                std::cout << "@ " << sc_core::sc_time_stamp() 
                          << " [DIGITAL_CTRL]: Alarm cleared. System nominal.\n";
            }
            wait();
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    // Mixed-Signal Boundary Signals
    sca_tdf::sca_signal<double> sig_temp("sig_temp"); // TDF-to-TDF
    sc_core::sc_signal<bool> sig_alarm("sig_alarm");  // TDF-to-DE
 
    // Instantiate Modules
    AnalogTempPhysics physics("physics");
    physics.temp_out(sig_temp);
 
    ThresholdComparator comparator("comparator");
    comparator.temp_in(sig_temp);
    comparator.alarm_out(sig_alarm);
 
    InterruptController ctrl("ctrl");
    ctrl.hw_alarm(sig_alarm);
 
    // Setup Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("boundary_wave");
    sca_util::sca_trace(tf, sig_temp, "Analog_Temperature");
    sca_util::sca_trace(tf, sig_alarm, "Digital_Alarm_Signal");
 
    // Start Simulation
    std::cout << "Starting Mixed-Signal Simulation...\n";
    sc_core::sc_start(15.0, sc_core::SC_SEC); // Simulate 15 seconds of physics
    
    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}

Best Practices for Modeler

  1. Minimize Crossings: Put analog-like signal processing (filters, integrations) entirely inside AMS clusters. Put digital control (state machines, registers) entirely inside SystemC modules.
  2. Compress Data: Use comparators or decimation filters inside the TDF domain to reduce the rate of data crossing into the DE domain.
  3. Document Timing Ownership: Clearly document which module acts as the timing master. In the example above, the TDF AnalogTempPhysics module sets the timestep, dictating exactly when the ThresholdComparator runs and when the DE boundary is evaluated.

Under the Hood: The sc_signal Equality Optimization

Why is it so crucial to use a ThresholdComparator rather than driving raw doubles into the DE domain? The answer lies in the C++ implementation of sc_core::sc_signal<T>::write(const T& value).

When the TDF converter port writes a value to a bound DE signal, the SystemC kernel executes an equality check: if (value == m_new_val) return;. If the value hasn't changed, the function returns immediately without scheduling an update request or triggering the value_changed_event().

If you were to route the raw continuous temperature (a double-precision float) directly into an sc_signal<double>, the value would be slightly different at every 10 ms timestep. The equality check would always fail, and the SystemC DE kernel would be forced to context switch, evaluate the signal, and wake up any sensitive digital threads 100 times a second.

By evaluating the threshold inside the TDF domain and driving an sc_signal<bool>, the TDF module repeatedly writes false to the port. The SystemC kernel detects false == false, instantly drops the write request, and allows the simulation to remain almost entirely within the high-speed, static TDF loop. The heavy DE event machinery only activates at the exact delta cycle where the boolean flips to true.

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