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_DIRis an absolute path with forward slashes, deliberately. Two constraints meet here. Passing it as a-Dthrough 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.