Timing model

A discrete-event simulation has a clock on the wall: self.now is the current time in seconds. This page is the forward model — how that time advances: the Clock, how interfaces charge for a transfer, and how a component charges for its own compute. Analyzing the resulting timeline (throughput, latency, overlap) is the separate Timing Analysis Tools section.

Clock

A Clock is a timing domain — just a frequency:

from waveflow.hw.clock import Clock

clk = Clock(freq=100e6)   # 100 MHz
clk.period                # 1e-8  seconds  (== 1.0 / freq)

A Clock is passed to each Interface object (and usually held on components as a clk field). It converts cycle counts — the natural unit for hardware — into the seconds the SimPy environment advances in.

Interfaces charge transfer latency

An Interface owns the latency model for data crossing it. The standard model is a fixed setup cost plus one cycle per word:

transfer_time = (latency_init + nwords) / clk.freq   [seconds]

latency_init captures wire delay / arbitration; each word adds one beat. So yielding a master.write(words) blocks the caller for transfer_time of simulated time. The per-interface parameters (latency_init, the FULL/LITE read/write formulas, latency_per_word) are documented with each interface — see Overview, streams, and memory-mapped. This page only notes that interfaces are where transfer time is charged.

Components charge compute latency

Transfer time alone is not the whole timeline — a component also spends time computing. Two ways to model that, both on SimObj:

Wait explicitly with self.timeout(delay)delay in seconds, so convert cycles via the clock:

def run_proc(self) -> ProcessGen[None]:
    # ... read input ...
    yield self.timeout(compute_cycles / self.clk.freq)   # model the compute latency
    # ... write output ...

Or wrap a window with self.action(name, processing_delay) — it advances time by processing_delay and records the window for analysis (start/end), so overlaps can be detected:

yield from self.action("decode", processing_delay=3 / self.clk.freq)

Each call appends an ActionRecord(name, start, end) to self.action_history; concurrent windows on the same object are collected in self.action_overlaps (count via self.active_overlap_count()). self.now is the current time at any point.

Pipelined transfers

For a component that streams data through (rather than buffering a whole burst), the stream endpoints expose get_pipelined / write_pipelined, which carry the first-word arrival time and the component’s initiation interval / latency (proc_ii, proc_latency, set to match HLS synthesis numbers). That mechanism is documented on the stream interface page.

What this feeds

These cycle costs are what make the simulated timeline meaningful — and what the Timing Analysis Tools then measure (and what the Logger records as timestamped events for later analysis).

See also

  • Timing Models — the deeper forward model: how a component’s load/compute/store shape (block, streaming, double-buffered) sets its timeline, building on this page.
  • Logging — capture self.now-stamped events to a CSV during the run.
  • Timing Analysis Tools — analyzing the timeline this model produces.
  • Interface Overview — the per-interface latency parameters.

This site uses Just the Docs, a documentation theme for Jekyll.