Flow steps

The concurrent flow starts from a Python component graph and ends at an exact cycle count measured by driving the real RTL. Two graphs go in — the DUT and its testbench — and the same graph walk generates both the kernel and its harness. Every step below is walked with its real code in the mem_copy example.

flowchart LR
  subgraph py["Python (source of truth)"]
    direction TB
    DUT["FreeRunMod graph<br/>(MemCopy: Sequencer→R→W)"]
    TB["composite testbench<br/>(MemCopyTB: DUT + BFM models)"]
  end

  DUT -->|"composite_top_spec<br/>+ render_top"| TOP["ap_ctrl_none top<br/>(one hls::task per child)"]
  TB  -->|"tb_top_spec<br/>+ render_tb_harness"| HARN["XSI harness<br/>(models, phases, run loop)"]

  TOP -->|"csynth"| RTL["Kernel RTL<br/>(ap_ctrl_none)"]
  RTL --> XSI["XSI sim<br/>(cycle-by-cycle in xsim)"]
  HARN --> XSI
  XSI --> CHK["bit-exact + exact cycle count"]

The steps

1 · The component graph. Describe the design as a FreeRunMod graph: standalone components that implement run_iter, and a composite that add_comps them, wires internal channels, and names its boundary. For mem_copy that is a Sequencer feeding a MemRStreamMemWStream over internal FIFOs. The testbench is also a graph — a composite FreeRunMod wiring the DUT to BFM participants (a driver, a sink, a shared memory) — and the same graph runs the pysim golden.

2 · Generate the kernel (composite_kernel). composite_top_spec walks the graph and render_top emits the ap_ctrl_none top: one hls::task per child, one internal FIFO per edge, boundary ports from the declared boundary. The task bodies come two ways — TaskBodyStep generates a leaf’s body from its run_iter; MemStreamStep copies the hand-written bodies that own an m_axi port (the dividing line is m_axi, not tops-vs-bodies).

3 · C-synthesis. The generated top is synthesized to RTL.

4 · Generate the testbench (sequential_xsi_tb). tb_top_spec walks the testbench graph and render_tb_harness emits the harness — which BFM models exist, which RTL port each drives, and the fixed-N cycle loop. The BFM model classes themselves are hand-written framework (xsi_bfm.h); the harness only wires them.

5 · XSI simulation. Because the kernel is ap_ctrl_none, Vitis co-sim refuses it — so the harness drives the elaborated RTL directly in xsim through XSI, cycle by cycle. The gate is exact: a bit-exact result and an exact cycle count (e.g. mem_copy = 2908 cycles for 16 jobs), so a count that moves is a real behaviour change.

The kernel and its testbench are generated from the same graphs that run the Python golden — one statement, two backends. That is what keeps the pysim model and the RTL from testing different things.

Source of truth: waveflow/build/composite_gen.py (composite_top_spec, render_top, tb_top_spec, render_tb_harness), waveflow/build/hwcodegen_steps.py (TaskBodyStep), waveflow/build/streamutils.py (MemStreamStep), tests/examples/test_xsi_bfm.py (the cycle gates).