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 dynamicsc_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