Chapter 8: SystemC AMS

AC and Noise Analysis in SystemC AMS

A deep dive into small-signal AC and noise analysis frequency-domain sweeps in SystemC AMS 2.0.

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.

AC and Noise Analysis in SystemC AMS

SystemC AMS 2.0 (IEEE 1666.1) standardized Small-Signal Frequency-Domain (AC) and Noise Analysis. Unlike traditional digital simulators that are strictly tied to the time domain, SystemC AMS allows you to perform exact, swept-frequency AC and Noise analyses on your abstract analog models.

This opens the door for system-level architects to verify frequency responses (like Bode plots, filter cut-offs, or amplifier bandwidths) and noise figures of abstract mixed-signal topologies before moving to SPICE or Verilog-A.

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 Kernel Reality: The AC Solver Bypass

When you execute an AC sweep in SystemC AMS, you completely bypass the standard SystemC discrete-event kernel. If you look at the Accellera SystemC AMS source code, calling sca_ac_analysis::sca_ac_start() invokes a specialized sca_ac_domain_solver.

Unlike time-domain simulations that execute the processing() callback continuously via delta cycles and time jumps, the sca_ac_domain_solver loops over the specified frequency range. At each frequency step, it calls the ac_processing() callback of every registered TDF module precisely once to construct and solve a steady-state complex linear equation matrix (A * x = b).

If a module lacks an ac_processing() implementation, the simulator typically assumes its AC output values are zero, making it an open circuit for AC analysis.

Inside ac_processing(), you do not write time-domain equations. Instead, you describe the complex, linear relationship between the inputs and the outputs at a given frequency.

Key sca_ac_analysis Utilities

When inside ac_processing(), you will heavily rely on the sca_ac_analysis namespace. Here are the critical tools that interface with the internal sca_ac_signal views:

  • sca_ac(port): Used to read a complex value from an input port or write a complex contribution to an output port.
  • sca_ac_w(): Returns the current angular frequency ($\omega$) in radians per second.
  • sca_ac_s(): Returns the Laplace complex frequency variable ($s = j\omega$).
  • sca_ac_z(): Returns the Z-domain complex frequency variable.
  • sca_ac_noise(noise_density, "name"): Injects frequency-dependent noise into the port. Under the hood, this registers an independent sca_noise_source in the solver matrix, calculating power spectral density contributions automatically.

Running an AC Sweep in sc_main()

To trigger an AC analysis, you use sca_ac_analysis::sca_ac_start() and sca_ac_analysis::sca_ac_noise_start() within sc_main(). These functions are highly configurable, allowing you to sweep frequencies linearly or logarithmically.

You also use standard tabular trace files (sca_util::sca_create_tabular_trace_file), but rather than tracing time, they will automatically trace the frequency axis.

Complete End-to-End Example

Below is a 100% complete and compilable example of a first-order Low-Pass Filter (LPF) implemented in TDF. The model defines both a time-domain processing() function and an ac_processing() function for AC/Noise sweeps.

#include <systemc-ams>
 
// 1. TDF Module for a Low-Pass Filter
SCA_TDF_MODULE(low_pass_filter) {
    sca_tdf::sca_in<double>  in;
    sca_tdf::sca_out<double> out;
 
    double f_cut; // Cut-off frequency
 
    SCA_CTOR(low_pass_filter) : in("in"), out("out"), f_cut(1.0e3) {}
 
    void set_attributes() {
        set_timestep(1.0, sc_core::SC_US); // 1 MHz sampling in time domain
    }
 
    void initialize() {
        // Laplace transfer function initialization for time-domain
        num(0) = 1.0;
        den(0) = 1.0;
        den(1) = 1.0 / (2.0 * M_PI * f_cut); 
    }
 
    // Time-domain processing
    void processing() {
        // Use the pre-defined continuous-time Laplace Transfer Function in TDF
        out.write( ltf_nd(num, den, in.read()) );
    }
 
    // AC and Noise frequency-domain processing
    void ac_processing() {
        // Retrieve the current Laplace variable s = j * w
        // Provided dynamically by the sca_ac_domain_solver per frequency step
        sca_util::sca_complex s = sca_ac_analysis::sca_ac_s();
        
        // Compute transfer function: H(s) = 1 / (1 + s / w_cut)
        double w_cut = 2.0 * M_PI * f_cut;
        sca_util::sca_complex H = 1.0 / (1.0 + s / w_cut);
 
        // Calculate AC response
        sca_util::sca_complex out_ac = H * sca_ac_analysis::sca_ac(in);
 
        // Add an arbitrary thermal noise floor (e.g., 1 nV / sqrt(Hz))
        sca_util::sca_complex noise = sca_ac_analysis::sca_ac_noise(1.0e-9, "thermal_noise");
        
        // Write the complex result out
        sca_ac_analysis::sca_ac(out) = out_ac + noise;
    }
 
private:
    sca_util::sca_vector<double> num;
    sca_util::sca_vector<double> den;
};
 
// 2. Simple AC Stimulus
SCA_TDF_MODULE(ac_source) {
    sca_tdf::sca_out<double> out;
 
    SCA_CTOR(ac_source) : out("out") {}
 
    void set_attributes() {
        set_timestep(1.0, sc_core::SC_US);
    }
 
    void processing() {
        // Time domain stimulus (e.g., DC bias or pulse)
        out.write(1.0); 
    }
 
    void ac_processing() {
        // Drive a 1V small-signal AC amplitude
        sca_ac_analysis::sca_ac(out) = sca_util::sca_complex(1.0, 0.0);
    }
};
 
// 3. System execution
int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_in("sig_in");
    sca_tdf::sca_signal<double> sig_out("sig_out");
 
    // Instantiation
    ac_source src("src");
    src.out(sig_in);
 
    low_pass_filter lpf("lpf");
    lpf.in(sig_in);
    lpf.out(sig_out);
    lpf.f_cut = 10.0e3; // 10 kHz cutoff
 
    // Setup frequency-domain tracing
    sca_util::sca_trace_file* ac_tf = sca_util::sca_create_tabular_trace_file("ac_results");
    
    // Trace complex outputs (SystemC AMS will log magnitude and phase automatically)
    sca_ac_analysis::sca_trace(ac_tf, sig_in, "input_node");
    sca_ac_analysis::sca_trace(ac_tf, sig_out, "output_node");
 
    std::cout << "Starting AC Analysis..." << std::endl;
    // Sweep from 1 Hz to 1 MHz, 100 points per decade, Logarithmic
    // Bypasses time-domain evaluation completely!
    sca_ac_analysis::sca_ac_start(1.0, 1.0e6, 100, sca_ac_analysis::SCA_LOG);
    
    std::cout << "Starting AC Noise Analysis..." << std::endl;
    // Sweep Noise over the same range
    sca_ac_analysis::sca_ac_noise_start(1.0, 1.0e6, 100, sca_ac_analysis::SCA_LOG);
 
    sca_util::sca_close_tabular_trace_file(ac_tf);
    
    // Note: sc_start() is not explicitly required if we only want AC analysis,
    // but you can call it to run time-domain simulations afterwards.
    // sc_core::sc_start(1.0, sc_core::SC_MS);
 
    return 0;
}

Analysis of the Run

  1. When sca_ac_start is called, the SystemC AMS kernel elaborates the model and evaluates ac_processing() across the defined frequency sweep.
  2. sca_ac(out) = H * sca_ac_analysis::sca_ac(in); dynamically scales the input source by the filter's transfer function at every frequency step.
  3. The results are dumped into ac_results.dat with frequency as the X-axis, allowing you to plot a perfect Bode diagram of your architecture.

Comments and Corrections