Linear Signal Flow (LSF)
Understanding the Linear Signal Flow (LSF) model of computation for modeling continuous-time control loops and filters.
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.
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.
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.
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.
Under the Hood: Symbolic Signals and the Linear Equation Matrix
Unlike TDF or standard SystemC DE where signals literally hold data payloads passing between callbacks, sca_lsf::sca_signal operates completely differently in C++.
In the Accellera implementation, an LSF signal is actually a symbolic node. When you instantiate sca_lsf::sca_add and connect it to a signal, no C++ mathematical functions are registered to execute dynamically. Instead, during the end_of_elaboration phase, the AMS Linear Solver parses the netlist and maps every node and primitive into a monolithic state-space mathematical matrix system:
A * dx(t) + B * x(t) + C * u(t) = 0
Each primitive simply contributes coefficients to the A, B, or C matrices. For example, an sca_integ contributes a differential row to A, while an sca_gain contributes an algebraic row to B.
During simulation, the AMS kernel evaluates the entire LSF cluster simultaneously by stepping through time using numerical integration algorithms (such as Trapezoidal or Gear methods) and solving the matrix via LU decomposition. This means the C++ primitives you connected structurally in your SC_CTOR vanish entirely at runtime, replaced by highly optimized internal linear algebra computations.
Comments and Corrections