Chapter 8: SystemC AMS

AMS TDF Rates, Delays, and Timesteps

How Timed Data Flow modules communicate through rates, delays, timesteps, sample timing, and solver scheduling.

AMS TDF Rates, Delays, and Timesteps

The Timed Data Flow (TDF) model of computation is highly efficient because it relies on static scheduling. The solver does not execute TDF modules dynamically based on hardware events; instead, it pre-calculates an execution order based on Rates, Delays, and Timesteps.

Understanding the mathematical relationship between these three attributes is the most important skill for an AMS engineer.

The Consistency Equation

The fundamental law of TDF is the consistency equation: $T_m = T_p \times R$

  • $T_m$ (Module Timestep): The physical time interval between two consecutive executions of a module's processing() callback.
  • $T_p$ (Port Timestep): The physical time interval between two consecutive samples arriving at (or leaving) a port.
  • $R$ (Rate): The number of samples read or written per processing() activation.

If a module executes every 10 ns ($T_m = 10$), and it writes 2 samples per execution ($R = 2$), the port timestep ($T_p$) MUST be 5 ns. The AMS solver automatically propagates these timesteps across the cluster. If it detects a mathematical contradiction (e.g., a 5 ns port connected to a 4 ns port without a rate conversion), it will halt elaboration with a fatal error.

Algebraic Loops and Delay

If you connect two TDF modules in a feedback loop (Module A drives Module B, and Module B drives Module A), the static scheduler cannot determine which module should execute first. This is called an algebraic loop.

To break an algebraic loop, you must explicitly insert a Delay using set_delay(N) on one of the ports. A delay of $N$ means that the port outputs $N$ initial samples before producing the first calculated sample. This acts like a $z^$ mathematical register, decoupling the dependency chain and allowing the solver to schedule the modules.

If you set a delay of $N$, you must implement the initialize() callback and initialize exactly $N$ samples on that port.

Complete Example: Fibonacci Feedback Loop

The following complete, compilable sc_main example demonstrates a classic algebraic loop: a Fibonacci sequence generator. It requires a feedback loop to add the previous two numbers, which forces us to use set_delay and initialize.

#include <systemc>
#include <systemc-ams.h>
 
// 1. A TDF Adder with a Delay to break the algebraic loop
SCA_TDF_MODULE(FibonacciAdder) {
    sca_tdf::sca_in<int>  in_prev;
    sca_tdf::sca_out<int> out_next;
 
    SCA_CTOR(FibonacciAdder) {}
 
    void set_attributes() {
        set_timestep(1.0, sc_core::SC_MS); // Execute every 1 ms
        
        // We are generating a feedback loop. 
        // We must delay the output by 1 sample to break the algebraic loop.
        // This effectively turns the output into a mathematical register (z^-1).
        out_next.set_delay(1);
    }
 
    void initialize() {
        // Because we declared a delay of 1, we MUST provide exactly 1 initial sample.
        // In the Fibonacci sequence, we start with 1.
        out_next.initialize(1, 0); // Initialize value 1 at index 0
    }
 
    void processing() {
        // Read the previous state from the feedback loop
        int prev_val = in_prev.read();
        
        // We need to keep track of the n-2 state internally to calculate the next
        static int n_minus_2 = 0;
        
        // Calculate the next Fibonacci number
        int next_val = prev_val + n_minus_2;
        
        // Update state
        n_minus_2 = prev_val;
        
        // Write the next value
        out_next.write(next_val);
        
        std::cout << "@ " << sc_core::sc_time_stamp() << " : " << next_val << "\n";
    }
};
 
// 2. A simple pass-through to complete the loop
SCA_TDF_MODULE(FeedbackPath) {
    sca_tdf::sca_in<int>  in;
    sca_tdf::sca_out<int> out;
 
    SCA_CTOR(FeedbackPath) {}
 
    void set_attributes() {
        // No explicit timestep needed; it propagates from FibonacciAdder
    }
 
    void processing() {
        // Simply pass the data back to the adder
        out.write(in.read());
    }
};
 
int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<int> sig_forward("sig_forward");
    sca_tdf::sca_signal<int> sig_feedback("sig_feedback");
 
    // Instantiate Modules
    FibonacciAdder adder("adder");
    adder.in_prev(sig_feedback);
    adder.out_next(sig_forward);
 
    FeedbackPath path("path");
    path.in(sig_forward);
    path.out(sig_feedback);
 
    std::cout << "Starting Fibonacci Generator...\n";
    
    // Run for 15 milliseconds (will generate 15 numbers)
    sc_core::sc_start(15.0, sc_core::SC_MS);
 
    return 0;
}

Best Practice

Keep TDF modules side-effect-light. Treat them strictly as signal processing blocks with explicit sample timing. If you need dynamic control logic (like starting or stopping the filter based on external interrupts), use standard SystemC discrete-event modules at the boundary to orchestrate the TDF cluster.

Comments and Corrections