Chapter 8: SystemC AMS

Linear Signal Flow (LSF)

Understanding the Linear Signal Flow (LSF) model of computation for modeling continuous-time control loops and filters.

Linear Signal Flow (LSF)

The Linear Signal Flow (LSF) model of computation is designed to model continuous-time, non-conservative systems. If you have ever used tools like Simulink to build block diagrams of mathematical transfer functions or PID controllers, you will feel right at home with the LSF MoC in SystemC AMS.

In LSF, a system is modeled as a directed graph where the nodes represent mathematical operations (addition, integration, differentiation, gain) and the edges represent continuous-time real-valued signals.

LSF Fundamentals

Unlike TDF, where you write custom C++ code in a processing() callback to define behavior, LSF relies entirely on a library of predefined primitive modules. You build your continuous-time equations by instantiating and physically connecting these primitives via signals.

Because LSF represents continuous time, the SystemC AMS solver aggregates all connected LSF primitives into a system of Differential and Algebraic Equations (DAEs). This entire equation system is solved together dynamically during the simulation.

Core LSF Primitives

The sca_lsf namespace provides the necessary primitives to construct your block diagrams. All primitives take inputs from sca_lsf::sca_in and drive outputs to sca_lsf::sca_out using sca_lsf::sca_signal channels.

  • sca_lsf::sca_add: Weighted addition of two signals: $y(t) = k_1 \cdot x_1(t) + k_2 \cdot x_2(t)$
  • sca_lsf::sca_sub: Weighted subtraction: $y(t) = k_1 \cdot x_1(t) - k_2 \cdot x_2(t)$
  • sca_lsf::sca_gain: Multiplies the input by a constant gain: $y(t) = k \cdot x(t)$
  • sca_lsf::sca_integ: Scaled time-domain integration: $y(t) = k \int x(t) dt + y_0$
  • sca_lsf::sca_dot: Scaled time derivative (differentiator).
  • sca_lsf::sca_ltf_nd: Laplace transfer function (Numerator/Denominator coefficients).

LSF Sources and Sinks

To feed discrete data into the continuous LSF domain, or to read data out of it, you must use converter primitives:

  • sca_lsf::sca_tdf::sca_source: Converts a TDF signal into a continuous LSF signal.
  • sca_lsf::sca_tdf::sca_sink: Samples an LSF signal and converts it back into a discrete TDF signal.

Complete Example: A Continuous-Time Feedback Loop

The following complete, compilable example demonstrates how to construct a continuous-time low-pass filter using a feedback loop with a subtractor and an integrator. A TDF square wave generator stimulates the filter, and the smoothed continuous response is converted back to TDF for tracing.

#include <systemc>
#include <systemc-ams.h>
 
// 1. A TDF Source generating a Square Wave
SCA_TDF_MODULE(SquareWaveSource) {
    sca_tdf::sca_out<double> out;
 
    SCA_CTOR(SquareWaveSource) {}
 
    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // 1 ms discrete timestep
    }
 
    void processing() {
        // Generate a 1 Hz square wave
        double t = get_time().to_seconds();
        double val = (std::fmod(t, 1.0) < 0.5) ? 1.0 : -1.0;
        out.write(val);
    }
};
 
// 2. The LSF Continuous-Time Filter
SC_MODULE(LSF_Filter) {
    // Interface to the discrete TDF world
    sca_tdf::sca_in<double>  in;
    sca_tdf::sca_out<double> out;
 
    // Internal LSF continuous-time signals
    sca_lsf::sca_signal lsf_in_sig;
    sca_lsf::sca_signal lsf_error_sig;
    sca_lsf::sca_signal lsf_out_sig;
 
    // Converter Primitives
    sca_lsf::sca_tdf::sca_source tdf_to_lsf;
    sca_lsf::sca_tdf::sca_sink   lsf_to_tdf;
 
    // LSF Math Primitives
    sca_lsf::sca_sub   subtractor;
    sca_lsf::sca_integ integrator;
 
    SC_CTOR(LSF_Filter)
        : tdf_to_lsf("tdf_to_lsf")
        , lsf_to_tdf("lsf_to_tdf")
        , subtractor("subtractor")
        , integrator("integrator", 10.0) // k_gain = 10.0 for integration
    {
        // 1. Convert TDF input to LSF
        tdf_to_lsf.inp(in);
        tdf_to_lsf.y(lsf_in_sig);
 
        // 2. Subtractor: Error = Input - Output (Feedback)
        subtractor.x1(lsf_in_sig);
        subtractor.x2(lsf_out_sig); // Feedback loop wired here
        subtractor.y(lsf_error_sig);
 
        // 3. Integrator: Output = Integral(Error) * 10.0
        integrator.x(lsf_error_sig);
        integrator.y(lsf_out_sig);
 
        // 4. Convert continuous LSF output back to discrete TDF
        lsf_to_tdf.x(lsf_out_sig);
        lsf_to_tdf.outp(out);
    }
};
 
int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_square("sig_square");
    sca_tdf::sca_signal<double> sig_filtered("sig_filtered");
 
    // Instantiate Modules
    SquareWaveSource src("src");
    src.out(sig_square);
 
    LSF_Filter filter("filter");
    filter.in(sig_square);
    filter.out(sig_filtered);
 
    // Setup Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("lsf_filter_wave");
    sca_util::sca_trace(tf, sig_square, "Input_SquareWave");
    sca_util::sca_trace(tf, sig_filtered, "Filtered_Continuous_Response");
 
    // Start Simulation
    sc_core::sc_start(3.0, sc_core::SC_SEC);
 
    sca_util::sca_close_vcd_trace_file(tf);
    return 0;
}

Key Takeaways from the LRM

  1. No Processing Callback: Notice that we did not write a processing() function in LSF_Filter. The behavior is entirely defined by instantiating primitives (like sca_sub and sca_integ) and binding their ports to sca_lsf::sca_signals.
  2. Algebraic Loops: LSF natively handles continuous-time feedback loops. In the example, lsf_out_sig is driven by the integrator but is simultaneously fed back into the x2 port of the subtractor. The AMS solver automatically resolves this loop during the simulation without requiring manual delay insertion (unlike TDF).
  3. Module Hierarchy: LSF models are wrapped in standard sc_core::sc_module classes, not sca_tdf::sca_module. This is because LSF primitives are themselves primitive leaf nodes; you use standard SystemC structural binding within SC_CTOR to connect them.

Comments and Corrections