Chapter 8: SystemC AMS

Dynamic TDF in SystemC AMS

A deep dive into Dynamic TDF (multirate varying timesteps and dynamic activation) 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.

Dynamic TDF in SystemC AMS

The original Timed Data Flow (TDF) model in SystemC AMS 1.0 was strictly static. Rates, delays, and timesteps were negotiated during elaboration and locked in place. This made simulation blazing fast, but made it exceptionally difficult to model systems with dynamic timing behavior—such as Pulse Width Modulation (PWM), event-driven state machines, clock jitters, and variable-frequency drives.

SystemC AMS 2.0 (IEEE 1666.1) introduced Dynamic TDF, which breaks the static scheduling paradigm. It allows a TDF module to dynamically request its next activation time, reacting instantly to external discrete events (DE) or internally calculated timelines.

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: Cluster Synchronization

To understand Dynamic TDF, you must look at the Accellera SystemC AMS cluster scheduling algorithm. In static TDF, the kernel builds a fixed execution list (sca_cluster_synchronization). Every module runs sequentially in an unrolled for loop.

When you opt-in to Dynamic TDF, you break this assumption. The kernel must now dynamically rebuild the cluster execution graph at runtime. If Module A requests a 2ms timestep, but it is connected to Module B which is forced to run at 1ms, the sca_sync_value_provider in the kernel must reconcile these differences. It does this by aggressively selecting the earliest requested time across the entire cluster, forcing all interconnected modules to execute early to guarantee data causality.

This dynamic graph recalculation incurs a performance penalty, which is why Dynamic TDF is strictly opt-in.

1. set_attributes() Configuration

Inside the set_attributes() callback, you use the following methods:

  • does_attribute_changes(): The module declares that it will dynamically alter its timing or rates.
  • accept_attribute_changes(): The module declares it can safely participate in a cluster where another module dynamically alters the schedule.

2. The change_attributes() Callback

Dynamic TDF introduces a completely new callback: change_attributes(). This callback is executed by the kernel's attribute evaluation phase immediately before the processing() phase. Inside this callback, you calculate the exact time the module should run next, overriding the static timestep.

3. request_next_activation()

Inside change_attributes(), you use request_next_activation() to notify the kernel's sca_cluster_synchronization:

  • request_next_activation(sc_core::sc_time): Schedule relative to the current time. The kernel internally schedules a dynamic sc_event.
  • request_next_activation(sc_core::sc_event): Schedule execution immediately when a specific DE event fires.

Complete End-to-End Example: Dynamic PWM Generator

Below is a complete, compilable example of a PWM (Pulse Width Modulator) that uses Dynamic TDF. Instead of running a fixed high-frequency TDF timestep (which is computationally expensive), the module dynamically calculates the exact time of the next rising or falling edge and yields control back to the scheduler.

#include <systemc-ams>
 
// Dynamic TDF PWM Generator
SCA_TDF_MODULE(dynamic_pwm) {
    sca_tdf::sca_in<double>  duty_cycle; // Input: 0.0 to 1.0
    sca_tdf::sca_out<double> pwm_out;    // Output: 0.0 or 1.0
 
    double period;          // Total period of the PWM signal
    double current_duty;    // Latched duty cycle
    bool   is_high;         // Current output state
 
    SCA_CTOR(dynamic_pwm) : duty_cycle("duty_cycle"), pwm_out("pwm_out"), 
                            period(10.0), current_duty(0.5), is_high(true) {}
 
    void set_attributes() {
        // 1. Opt-in to Dynamic TDF
        // Alerts the AMS kernel that this module's sca_node will shift time
        does_attribute_changes();
        
        // Ensure the input port also accepts the dynamic scheduling changes
        duty_cycle.set_timestep(1.0, sc_core::SC_MS);
    }
 
    // 2. The dynamic attributes callback (Executes before processing)
    void change_attributes() {
        // Calculate sleep time based on the state
        double time_to_next_edge = 0.0;
 
        if (is_high) {
            // We are high, wait for the duty cycle duration
            time_to_next_edge = period * current_duty;
        } else {
            // We are low, wait for the remainder of the period
            time_to_next_edge = period * (1.0 - current_duty);
        }
 
        // Clamp the time to avoid zero-delay loops on 0% or 100% duty cycles
        if (time_to_next_edge < 0.001) time_to_next_edge = 0.001;
 
        // 3. Request activation precisely at the next edge
        request_next_activation(sc_core::sc_time(time_to_next_edge, sc_core::SC_MS));
    }
 
    // 4. The processing callback runs exactly when requested
    void processing() {
        // If we are at the start of a period (transitioning to high), read new duty cycle
        if (!is_high) {
            current_duty = duty_cycle.read();
            // Clamp duty cycle for safety
            if (current_duty > 1.0) current_duty = 1.0;
            if (current_duty < 0.0) current_duty = 0.0;
        }
 
        // Toggle state
        is_high = !is_high;
 
        // Write output
        if (is_high) {
            pwm_out.write(1.0);
        } else {
            pwm_out.write(0.0);
        }
    }
};
 
// Simple stimulus generating a varying duty cycle
SCA_TDF_MODULE(duty_stimulus) {
    sca_tdf::sca_out<double> duty_out;
 
    SCA_CTOR(duty_stimulus) : duty_out("duty_out") {}
 
    void set_attributes() {
        // We must accept dynamic timing shifts from the PWM generator
        // This alerts the sca_sync_value_provider that this module is cluster-safe
        accept_attribute_changes();
        // Give a default static timestep, though it will be driven dynamically
        set_timestep(1.0, sc_core::SC_MS);
    }
 
    void processing() {
        // Slowly increase the duty cycle over time
        double time_ms = sc_core::sc_time_stamp().to_seconds() * 1000.0;
        double duty = 0.1 + (time_ms / 100.0) * 0.1; 
        if (duty > 0.9) duty = 0.9;
        
        duty_out.write(duty);
    }
};
 
int sc_main(int argc, char* argv[]) {
    // Signals
    sca_tdf::sca_signal<double> sig_duty("sig_duty");
    sca_tdf::sca_signal<double> sig_pwm("sig_pwm");
 
    // Instantiation
    duty_stimulus stim("stim");
    stim.duty_out(sig_duty);
 
    dynamic_pwm pwm("pwm");
    pwm.duty_cycle(sig_duty);
    pwm.pwm_out(sig_pwm);
    pwm.period = 10.0; // 10 ms period
 
    // Tracing
    sca_util::sca_trace_file* tf = sca_util::sca_create_tabular_trace_file("pwm_trace");
    sca_util::sca_trace(tf, sig_duty, "duty_cycle");
    sca_util::sca_trace(tf, sig_pwm, "pwm_out");
 
    std::cout << "Starting Dynamic TDF Simulation..." << std::endl;
    sc_core::sc_start(100.0, sc_core::SC_MS); // Simulate for 100 ms
 
    sca_util::sca_close_tabular_trace_file(tf);
    
    std::cout << "Simulation finished. Inspect pwm_trace.dat." << std::endl;
 
    return 0;
}

Why is this powerful?

In standard static TDF, to accurately model a PWM signal with 1% resolution on a 10ms period, you would be forced to run the processing() callback every 0.1 ms, resulting in 1,000 executions over 100ms. With Dynamic TDF, the module pushes a targeted sc_event to the kernel and sleeps entirely between edges, executing processing() only twice per period (20 executions total), regardless of the duty cycle resolution. This bypasses massive amounts of cluster evaluation overhead while maintaining perfect temporal accuracy.

Comments and Corrections