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
- No Processing Callback: Notice that we did not write a
processing()function inLSF_Filter. The behavior is entirely defined by instantiating primitives (likesca_subandsca_integ) and binding their ports tosca_lsf::sca_signals. - Algebraic Loops: LSF natively handles continuous-time feedback loops. In the example,
lsf_out_sigis driven by the integrator but is simultaneously fed back into thex2port of the subtractor. The AMS solver automatically resolves this loop during the simulation without requiring manual delay insertion (unlike TDF). - Module Hierarchy: LSF models are wrapped in standard
sc_core::sc_moduleclasses, notsca_tdf::sca_module. This is because LSF primitives are themselves primitive leaf nodes; you use standard SystemC structural binding withinSC_CTORto connect them.
Comments and Corrections