Timed Data Flow (TDF)
A deep dive into the Timed Data Flow (TDF) model of computation, including ports, modules, attributes, and processing phases.
Timed Data Flow (TDF)
The Timed Data Flow (TDF) model of computation is the most commonly used MoC in SystemC AMS. It is designed to model signal processing algorithms, feedback loops, and communication systems using discrete-time sampling.
TDF is highly efficient because the activation schedule of a set of connected TDF modules is computed statically before the simulation starts. The solver analyzes the number of samples read and written by each module (the rates) and builds a fixed, deterministic schedule that minimizes overhead.
The Structure of a TDF Module
According to the IEEE 1666.1 standard, a TDF module must derive from sca_tdf::sca_module (or use the handy SCA_TDF_MODULE macro). Unlike standard SystemC modules, TDF modules do not use SC_METHOD or SC_THREAD. Instead, they define their behavior using specific virtual callbacks:
set_attributes(): Called during the elaboration phase. This is where you configure the properties of the module and its ports (like time steps, rates, and delays).initialize(): Called exactly once at the beginning of the simulation. Used to set initial conditions or to write initial delay samples into output ports.processing(): The core time-domain behavior of the module. This is called repeatedly during simulation according to the statically computed schedule.change_attributes()(Dynamic TDF): Evaluated during simulation to allow dynamic adaptation of timesteps or rates (introduced in AMS 2.0).
Core TDF Attributes: Timestep, Rate, and Delay
To build the static schedule, the TDF solver relies on three fundamental attributes assigned to ports and modules during set_attributes():
- Timestep (
set_timestep): The time interval between two consecutive samples. If assigned to a module, it defines the period at which theprocessing()function is activated. - Rate (
set_rate): The number of samples read or written to a port per module activation. By default, the rate is 1. If a port has a rate of $R$, the module must read/write exactly $R$ samples every timeprocessing()is called. - Delay (
set_delay): The number of samples inserted at a port before the first actual output sample is produced. This acts as a mathematical register (z^-1) and is absolutely required to break algebraic loops in feedback circuits.
The Consistency Equation
For the TDF solver to function, the assigned timesteps and rates must be mathematically consistent across the entire cluster. The relationship between a module's timestep ($T_m$), a port's timestep ($T_p$), and the port's rate ($R$) is strictly defined as:
$$T_m = T_p \times R$$
Complete Example: Multirate TDF Mixer
This complete, compilable example demonstrates a classic communication system block: a Mixer. It takes an RF input and a Local Oscillator (LO) input, multiplies them to produce an Intermediate Frequency (IF), and then uses a Multirate Decimator to downsample the output.
#include <systemc>
#include <systemc-ams.h>
// 1. A Simple TDF Source
SCA_TDF_MODULE(Oscillator) {
sca_tdf::sca_out<double> out;
double frequency;
SCA_CTOR(Oscillator) : frequency(1e6) {} // 1 MHz default
void set_attributes() {
set_timestep(0.1, sc_core::SC_US); // 10 MHz sampling rate
}
void processing() {
double t = get_time().to_seconds();
out.write(std::sin(2.0 * M_PI * frequency * t));
}
};
// 2. The Mixer
SCA_TDF_MODULE(Mixer) {
sca_tdf::sca_in<double> rf_in;
sca_tdf::sca_in<double> lo_in;
sca_tdf::sca_out<double> if_out;
SCA_CTOR(Mixer) {}
void set_attributes() {
// We do not need to set the timestep here.
// The solver will automatically inherit the 0.1us timestep
// from the oscillators connected to our inputs.
}
void processing() {
// Rates default to 1. We read exactly one sample from each input,
// and write exactly one sample to the output.
double rf_val = rf_in.read();
double lo_val = lo_in.read();
if_out.write(rf_val * lo_val);
}
};
// 3. A Multirate Decimator (Downsampler)
SCA_TDF_MODULE(Decimator) {
sca_tdf::sca_in<double> in;
sca_tdf::sca_out<double> out;
SCA_CTOR(Decimator) {}
void set_attributes() {
// We will read 4 samples for every 1 sample we write out.
// This makes our output port timestep 4x slower than our input port timestep.
in.set_rate(4);
out.set_rate(1);
// We add a delay of 1 sample to the output to demonstrate breaking algebraic loops
out.set_delay(1);
}
void initialize() {
// Because we set a delay of 1 on 'out', we MUST write 1 initial sample
// during initialize() to prime the delay buffer.
out.initialize(0.0);
}
void processing() {
double sum = 0;
// We MUST read exactly 'in.get_rate()' samples (4 samples)
for (unsigned int i = 0; i < in.get_rate(); ++i) {
sum += in.read(i); // Read at specific index
}
// We write exactly 'out.get_rate()' samples (1 sample)
double average = sum / 4.0;
out.write(average);
}
};
int sc_main(int argc, char* argv[]) {
// Signals
sca_tdf::sca_signal<double> sig_rf("sig_rf");
sca_tdf::sca_signal<double> sig_lo("sig_lo");
sca_tdf::sca_signal<double> sig_mixed("sig_mixed");
sca_tdf::sca_signal<double> sig_downsampled("sig_downsampled");
// Modules
Oscillator rf_osc("rf_osc");
rf_osc.frequency = 2.1e6; // 2.1 MHz
rf_osc.out(sig_rf);
Oscillator lo_osc("lo_osc");
lo_osc.frequency = 2.0e6; // 2.0 MHz
lo_osc.out(sig_lo);
Mixer mixer("mixer");
mixer.rf_in(sig_rf);
mixer.lo_in(sig_lo);
mixer.if_out(sig_mixed);
Decimator dec("dec");
dec.in(sig_mixed);
dec.out(sig_downsampled);
// Tracing
sca_util::sca_trace_file* tf = sca_util::sca_create_vcd_trace_file("tdf_multirate");
sca_util::sca_trace(tf, sig_rf, "RF_In");
sca_util::sca_trace(tf, sig_lo, "LO_In");
sca_util::sca_trace(tf, sig_mixed, "Mixed_IF");
sca_util::sca_trace(tf, sig_downsampled, "Downsampled_IF");
sc_core::sc_start(10.0, sc_core::SC_US);
sca_util::sca_close_vcd_trace_file(tf);
return 0;
}Key LRM Takeaways
- Timestep Propagation: You do not need to assign a timestep to every module. The AMS solver automatically propagates timesteps across connected clusters using the consistency equation.
- Delay Initialization: If you apply
set_delay(N)to a port, you must call.initialize(value, index)exactly $N$ times within theinitialize()callback to prime the solver's buffers. - Multirate Reads/Writes: If a port has a rate $> 1$, you must use the indexed version of read/write (e.g.,
in.read(i)) and process exactly the required number of samples duringprocessing().
Comments and Corrections