Testbench codegen — what is generated, and what is not

memcpy generates its whole XSI harness by walking the testbench graph (Testbench codegen): each participant maps to a BFM twin, and the harness is derivable because the test is a graph. This example does not work that way, and the difference is worth stating rather than leaving a reader to notice the missing page.

vec_mult_tb.cpp is hand-written and committed. A csim testbench is a main() that reads two files, calls a function, and compares — there is no graph to walk, no participants to instantiate, and no ports to bind. Generating it would mean inventing a description of something already shorter than its own description.

What is generated is the small part that can go wrong silently: the numbers the testbench and the DUT must agree about.

vec_mult_params.h — the anti-drift header

Emitted by generate_dut, in the same step that writes the top:

// GENERATED by vecmult_build.generate_dut -- do not edit.
#define VM_DWID 64
#define VM_VLEN 4096
#define VM_DATA_DIR "C:/Users/sdran/Documents/repos/pysilicon/examples/vecmult/data"
#endif

The two knobs are here rather than restated in the testbench for the obvious reason and one less obvious one. The obvious one: a TB compiled at DWID=64 against a body instantiated at 256 would mis-pack every word, and would do it quietly — the lane serializer would simply read the wrong number of samples per word. The less obvious one: these are exactly the two values a sweep varies, so they change 16 times in a run. Anything restated changes 16 chances to drift into 16 chances to be wrong.

VM_DATA_DIR is an absolute path with forward slashes, deliberately. Two constraints meet here. Passing it as a -D through TCL does not survive the quoting on Windows; and csim runs from <proj>/<solution>/csim/build, so a relative path would silently depend on how deep that happens to be. Baking a resolved path into a generated header sidesteps both. It is also why this header is a build product and not something you would ever commit.

The testbench calls the task, not the top

#include "vec_mult_params.h"
#include "vec_mult_task.h"
...
vec_mult_task<VM_DWID, VM_VLEN>(s_in, z_out);

Not vec_mult(...). The generated top is ap_ctrl_none wrapping an hls::task, which by construction never returns — csim of it would spin forever. That is not a limitation being worked around: the task body is the artifact under test, and the top the DUT codegen emits is derived and carries no arithmetic at all. Simulating it would add a wrapper to the thing being checked and nothing else.

The vectors come from the pysim rung

PySimStep runs one firing in SimPy, checks it against golden(), and then writes the same stimulus and expectation into data/. The csim rung replays exactly those files.

That direction matters. A testbench that computed its own expected output would be checking the C++ against a second C++ definition of the same arithmetic — which is the failure the shared golden() exists to prevent, reintroduced one layer down. Because the vectors are written by the rung that runs the Python body, WAVEFLOW_CSIM_OK means the two languages agree, not that the C++ is self-consistent.

The pysim step also picks the length deliberately: n = vlen - 3, which is neither vlen nor a multiple of LW, so every csim run exercises the ragged final beat. A full-length vector never reaches it.

Two TCLs, not one

file written by what it does
vec_mult.tcl generated by render_tcl csynth_design on gen/vec_mult.cpp
vec_mult_csim.tcl hand-written, committed csim_design on the body plus vec_mult_tb.cpp

The generated TCL has no add_files -tb hook, so it cannot run a simulation; the csim TCL adds the testbench and never touches the top. Keeping them apart is what lets csynth consume the csim verdict as a DAG artifact rather than the two sharing a project directory and a solution.

The csim rung looks for WAVEFLOW_CSIM_OK in the output and treats its absence as failure, so a run that crashed, was skipped, or printed nothing is a failure rather than a pass — the same reasoning as deleting a trace before requiring it to reappear.

Next

  • The sweep — 16 design points through this DAG, and the grid that separates the two BRAM regimes.
  • Testbench — the pysim harness and the twin check this codegen serves.