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.