Chapter 8: SystemC AMS

AMS Modeling Style for Virtual Platforms

How to combine SystemC AMS with a VP without turning a software platform model into a slow analog simulation.

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 Modeling Style for Virtual Platforms

SystemC AMS can enrich a Virtual Platform (VP), but it should not accidentally change the purpose of the VP. Virtual platforms are primarily designed for fast software execution, whereas analog simulations often require fine-grained time steps that slow down simulation significantly.

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.

When AMS Belongs in a VP

According to the SystemC AMS standard (IEEE 1666.1), you should use AMS when software-visible behavior depends on analog or signal-processing effects:

  • ADC sample streams
  • Sensor thresholds
  • PLL lock approximation
  • Power or thermal trends
  • Filters
  • Motor-control feedback

Do not use AMS merely to make the model look more advanced.

Good VP Boundary

A practical mixed VP usually has:

  • A TLM register block for software programming.
  • An AMS cluster (TDF or LSF) for signal behavior.
  • Converter ports (e.g., sca_tdf::sca_in, sca_tdf::sca_out connected to sc_core::sc_signal) for control and status between the AMS and discrete-event worlds.
  • Interrupts or status registers for software-visible results.

The software should still see registers, memory, and interrupts, modeled via standard TLM-2.0 or Simple Bus abstraction.

Complete AMS to SystemC Boundary Example

Below is a fully compilable sc_main program demonstrating how to interface an AMS TDF (Timed Data Flow) model representing an ADC with a standard SystemC discrete-event module representing a simple Virtual Platform peripheral register block.

#include <systemc>
#include <systemc-ams>
 
// 1. AMS TDF Module (The Analog / Continuous Part)
SCA_TDF_MODULE(adc_sensor) {
    // Converter port: Continuous output to Discrete Event (DE) domain
    sca_tdf::sca_out<double> analog_out; 
    
    // Internal state
    double current_val;
 
    SCA_CTOR(adc_sensor) : analog_out("analog_out"), current_val(0.0) {}
 
    void set_attributes() override {
        // Set a coarse timestep so we don't slow down the VP unnecessarily
        set_timestep(1.0, sc_core::SC_MS);
    }
 
    void processing() override {
        // Simulate a slowly changing analog value (e.g., a temperature sensor)
        current_val += 0.5;
        if (current_val > 100.0) {
            current_val = 0.0;
        }
        // Write out the analog value to the converter port
        analog_out.write(current_val);
    }
};
 
// 2. VP Peripheral (The Discrete Event / Software Visible Part)
SC_MODULE(vp_adc_peripheral) {
    // Input from the AMS domain
    sc_core::sc_in<double> analog_in; 
    
    // Interrupt output to the CPU
    sc_core::sc_out<bool> irq_out;
 
    // A simple threshold register programmable by software
    double threshold_reg;
 
    SC_CTOR(vp_adc_peripheral) : analog_in("analog_in"), irq_out("irq_out"), threshold_reg(50.0) {
        SC_METHOD(monitor_threshold);
        sensitive << analog_in; // Trigger whenever the AMS converter writes a new value
        dont_initialize();
    }
 
    void monitor_threshold() {
        double current = analog_in.read();
        
        // If the analog value crosses the software-programmed threshold, trigger an interrupt
        if (current >= threshold_reg) {
            irq_out.write(true);
            std::cout << "@ " << sc_core::sc_time_stamp() 
                      << " VP ADC: Threshold crossed! Value: " << current 
                      << ", raising IRQ." << std::endl;
        } else {
            irq_out.write(false);
        }
    }
};
 
// 3. Top-Level Integration
int sc_main(int argc, char* argv[]) {
    // Signals
    sc_core::sc_signal<double> analog_sig("analog_sig");
    sc_core::sc_signal<bool> irq_sig("irq_sig");
 
    // Instantiation
    adc_sensor ams_block("ams_block");
    vp_adc_peripheral vp_block("vp_block");
 
    // Binding
    ams_block.analog_out(analog_sig);
    vp_block.analog_in(analog_sig);
    vp_block.irq_out(irq_sig);
 
    // Run simulation
    std::cout << "Starting mixed AMS/VP simulation..." << std::endl;
    sc_core::sc_start(200.0, sc_core::SC_MS);
    std::cout << "Simulation finished." << std::endl;
 
    return 0;
}

Performance Rule

Keep AMS timesteps as coarse as the use case allows. A VP that needs to boot firmware should not spend most of its time solving unnecessary high-resolution analog detail. In the example above, a 1.0 ms timestep was used to minimize context switches between the AMS solver and the SystemC kernel.

Under the Hood: Dynamic TDF (request_next_activation)

In standard TDF, the internal SC_THREAD loops rigidly at cluster_period intervals. Even if the analog signal isn't changing or the software isn't reading the ADC, the SystemC scheduler is continually interrupted to execute the solver matrix.

To optimize VPs, the Accellera implementation of SystemC AMS 2.0 introduced Dynamic TDF. Inside your processing() callback, you can explicitly call the C++ method request_next_activation(sc_event).

When you make this call, you are signaling the AMS cluster thread to abandon its fixed static schedule for this module. The cluster thread internally yields and does not automatically wake up on the next timestep. Instead, it waits indefinitely until the standard sc_core::sc_event is fired.

For example, if a TLM-2.0 b_transport read arrives from the digital CPU, the TLM socket can fire an sc_event. The TDF module wakes up, computes a single analog sample, provides the result back to the TLM thread, and goes back to sleep. This "demand-driven" analog evaluation means the AMS solver consumes zero CPU cycles while the software VP is executing unrelated code, drastically accelerating boot times.

Documentation Rule

For each AMS block in a VP, document:

  • Model of computation (e.g., TDF, LSF, ELN)
  • Timestep and rates
  • Delays
  • Boundary ports (Converter type)
  • Numerical approximations
  • Software-visible effects (IRQs, specific register behaviors)

This lets users understand what is accurate, approximate, and intentionally ignored.

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