DUT codegen — the graph becomes an hls::task top
The Python model is a graph: three leaves, two framed edges, four boundary ports. Generating the DUT kernel is walking that graph and lowering it to a free-running Vitis HLS top.
What an hls::task is
Vitis HLS hls::task is the free-running execution model. A task is a body that the runtime re-fires
on its own whenever its input streams have data — there is no host start/done handshake, so the
top is declared ap_ctrl_none. Tasks connected by hls::stream FIFOs run concurrently and
overlap: one task processes job j+1 while another is still on job j. That is exactly the
concurrent flow’s model, and it is why mem_copy’s three stages —
sequence, read, write — become three tasks wired by FIFOs rather than one sequential function.
The contrast is ap_ctrl_hs (the sequential flow): a kernel the
host launches once and waits on. A free-running task cannot be driven that way — which is also why it
cannot be verified by Vitis C/RTL cosim, and is instead run through XSI.
How a FreeRunMod lowers to a task top
Each leaf FreeRunMod becomes one hls::task; each add_if edge becomes one hls_thread_local
FIFO; the boundary list becomes the interface pragmas. composite_top_spec reads the
sub-components’ kernel_task() signatures and the edges, resolves each task argument to a boundary port
or an internal FIFO, and render_top emits it. Nothing about the top is hand-written — it is the
graph made concrete:
// examples/mem_copy/gen/mem_copy.cpp — GENERATED
void mem_copy(hls::stream<ap_uint<64> >& s_cmd, const ap_uint<64>* m_in,
ap_uint<64>* m_out, hls::stream<ap_uint<64> >& s_done) {
#pragma HLS INTERFACE axis port=s_cmd
#pragma HLS INTERFACE m_axi port=m_in offset=slave bundle=gmem0 depth=8192
#pragma HLS INTERFACE m_axi port=m_out offset=slave bundle=gmem1 depth=8192
#pragma HLS INTERFACE axis port=s_done
#pragma HLS INTERFACE ap_ctrl_none port=return
hls_thread_local hls::stream<streamutils::framed_word<64> > cmd; // Sequencer -> reader
hls_thread_local hls::stream<streamutils::framed_word<64> > copy_data; // reader -> writer
hls_thread_local hls::task t0(mem_seq_framed_task<64>, s_cmd, cmd);
hls_thread_local hls::task t1(mem_r_stream_framed_task<64>, cmd, m_in, copy_data);
hls_thread_local hls::task t2(mem_w_stream_framed_done_task<64, 8>, copy_data, m_out, s_done);
}
Each edge is a framed_word FIFO (a StreamIF(framed=True)); each m_axi bundle is assigned by
policy in boundary order (m_in→gmem0, m_out→gmem1); the writer’s max_fwd_words=8 rides along
as a second template argument. There is no #define and no while: the width is a template argument,
and the task runtime supplies the re-firing.
The bodies are all hand-written
The top generates; every task body does not. MemStreamStep copies the three fixed headers
(mem_seq_framed_task.h, mem_r_stream_framed_task.h, mem_w_stream_framed_done_task.h) verbatim into
include/. Two reasons put a body on the hand-written side of the line:
- it owns
m_axi— the mem-stream read/write bodies do, and task-body emission refuses a memory port (bundle naming, depth, and the offset register are decisions the emitter does not make); or - it constructs descriptors / drives a framed channel — the
Sequencerdoes both, neither of which is in the code generator’s vocabulary.
So for mem_copy the rule is simply: the top generates, the bodies are copied. Each body’s
run_iter (Python model) is a pysim golden whose only tie to its C++ is a test.
Task-body generation still exists. A stream-only leaf whose
run_iteris justget→@synthesizablehook →writecan have its body generated fromrun_iterbyTaskBodyStep— a generated stream-only compute pipeline is built that way.mem_copysimply has no such leaf.
Building it
codegen_dut is the build step that emits all of this — the top, its csynth .tcl, the port map, and
the headers:
python examples/mem_copy/mem_copy_build.py --through codegen_dut
codegen_dut:
gen\mem_copy.cpp
mem_copy.tcl
xsi\mem_copy_ports.h
RUNNING...
generated DUT gen\mem_copy.cpp + mem_copy.tcl + xsi/mem_copy_ports.h
PASSED
gen/mem_copy.cpp is the top above; mem_copy.tcl drives Vitis HLS C-synthesis (the csynth step,
which needs Vitis and produces the RTL the RTL rung drives); xsi/mem_copy_ports.h is
the DUT’s port map, which the generated testbench harness includes. Anything
generated carries a banner and a regenerate overwrites a hand-edit — so don’t hand-edit them.
Next
Testbench codegen — how the MemCopyTB graph becomes the XSI BFM harness that
drives this top.