Vitis HLS Code Generation
The same HistAccel Python class that ran in SimPy is the source the Vitis HLS
kernel is generated from. Waveflow emits everything mechanical — the m_axi
function signature, the interface pragmas, the multi-buffer burst calls, the
validation-status plumbing — and leaves only the datapath hooks as hand-written
C++. This page walks the generated kernel, the generated testbench, and the seam
between generated and hand-written code.
Running the generation
For the histogram example the generation is a small helper,
generate_vitis_sources, in
hist_build.py. It
writes the support headers, then lowers the kernel and the testbench:
# examples/shared_mem/hist_build.py — generate_vitis_sources
(gen / "hist.cpp").write_text(kernel_to_cpp(HistAccel))
(gen / "hist.hpp").write_text(header_to_cpp(HistAccel))
for fname, content in tb_files_to_str(HistTBHls).items():
(gen / fname).write_text(content) # gen/hist_tb.cpp
kernel_to_cpp(HistAccel) lowers the run_proc body; tb_files_to_str(HistTBHls)
lowers the testbench’s main(). Both come from the same Python source you
already simulated — there is no second description of the kernel to keep in sync.
After it runs, gen/ holds three framework-owned files, and three hand-written
hook files sit next to the Python source:
| File | Owner | Lifecycle |
|---|---|---|
gen/hist.hpp |
framework | rewritten each run |
gen/hist.cpp |
framework | rewritten each run |
gen/hist_tb.cpp |
framework | rewritten each run |
hist_validate_impl.cpp |
user | committed — the validation logic |
hist_compute_impl.cpp |
user | committed — the binning datapath |
hist_respond_impl.tpp |
user | committed — the response write |
gen/ is .gitignored — generated files are never committed; the hooks are
committed because they hold logic no codegen pass reproduces.
The kernel signature and pragmas
The generated top-level function is where the three Python ports become the three AXI interfaces:
// gen/hist.cpp
void hist(
hls::stream<streamutils::axi4s_word<32>>& s_in,
hls::stream<streamutils::axi4s_word<32>>& m_out,
ap_uint<32>* m_mem
) {
#pragma HLS INTERFACE axis port=s_in
#pragma HLS INTERFACE axis port=m_out
#pragma HLS INTERFACE m_axi port=m_mem offset=slave bundle=gmem depth=m_mem_depth
#pragma HLS INTERFACE ap_ctrl_hs port=return
s_in/m_outbecome AXI4-Stream ports (axis) — the command in, the response out.m_membecomes the AXI memory-mapped master: a plainap_uint<32>*with anm_axipragma.offset=slavemeans the base address is itself programmed over the control interface;bundle=gmemputs all of this kernel’s memory traffic on one shared port.-
depth=m_mem_depthsizes the interface for co-simulation. That constant is the sum of the buffer maxima — the worst-case number of words the kernel could touch:// gen/hist.hpp static const int m_mem_depth = max_ndata + max_nbins + max_nbins;i.e.
data(max_ndata) +edges(max_nbins) +counts(max_nbins). Themax_ndata/max_nbinsHwParams from the Python model drive this directly. ap_ctrl_hsis the start/done handshake — the same control contract as the regmap example, here implicit on the kernel rather than exposed as registers.
The body: validate, then multi-buffer bursts
The lowered run_proc reads exactly like the Python it came from:
// gen/hist.cpp
HistCmd cmd;
cmd.read_axi4_stream<32>(s_in);
ap_uint<8> status = hist_impl::validate(cmd);
if (status != (ap_uint<8>)static_cast<unsigned int>(HistError::NO_ERROR)) {
hist_impl::respond(m_out, cmd.tx_id, status);
return;
}
static float data[max_ndata];
float32_array_utils::read_array_slice<32>(m_mem + memmgr::byte_addr_to_word_index<32>(cmd.data_addr), 0, cmd.ndata, data);
static float edges[max_nbins];
float32_array_utils::read_array_slice<32>(m_mem + memmgr::byte_addr_to_word_index<32>(cmd.bin_edges_addr), 0, cmd.nbins - 1, edges);
static ap_uint<32> counts[32];
hist_impl::compute(data, edges, cmd.ndata, cmd.nbins, counts);
uint32_array_utils::write_array_slice<32>(counts, m_mem + memmgr::byte_addr_to_word_index<32>(cmd.cnt_addr), 0, cmd.nbins);
hist_impl::respond(m_out, cmd.tx_id, status);
The pieces this example exists to exercise:
- Multi-buffer, multi-type lowering. Three
read_array_slice/write_array_slicecalls target the onem_mempointer at three different command addresses, with two element types —float32_array_utils::read_array_slicefordataandedges,uint32_array_utils::write_array_sliceforcounts. Each Pythonread_array(Float32, ...)/write_array(Uint32Field, ...)chose its array-utils namespace from the schema’s element type. - Byte address → word index. Every burst wraps the command’s byte address in
memmgr::byte_addr_to_word_index<32>(...)before indexing the word pointer — the same conversion the SimPyDirectMMIFdid. - The runtime count is the Python expression, verbatim.
cmd.ndataandcmd.nbins - 1are lowered straight from the model’sread_arrayarguments — including thenbins - 1edge count, a no-op burst whennbins == 1. - Compute returns an array via an out-parameter. HLS cannot return an array
by value, so the generator declares the
static ap_uint<32> counts[...]buffer in the kernel and passes it tohist_impl::compute(..., counts)— the Pythoncompute“returns” the counts; the C++ fills them in place. - Validation is a status, checked once.
validatereturns anap_uint<8>; the!=againstNO_ERRORis the one branch, and on failure the kernel responds and returns before touching memory.
The header also declares the three hooks the body calls into:
// gen/hist.hpp
namespace hist_impl {
ap_uint<8> validate(HistCmd cmd);
template <int out_bw>
void respond(hls::stream<streamutils::axi4s_word<out_bw>>& m_out, int tx_id, ap_uint<8> status);
void compute(float data[1024], float edges[32], int ndata, int nbins, ap_uint<32> out[32]);
}
The hand-written datapath
The hooks supply the bodies. hist_compute_impl.cpp is the binning datapath — the
loop that turns samples into counts:
// examples/shared_mem/hist_compute_impl.cpp — hist_impl::compute
for (int i = 0; i < ndata; ++i) {
#pragma HLS LOOP_TRIPCOUNT min=1 max=max_ndata
float sample = data[i];
int bin = 0;
hist_search:
for (int b = 0; b < nbins - 1; ++b) {
#pragma HLS LOOP_TRIPCOUNT min=1 max=max_nbins
#pragma HLS PIPELINE II=1
if (sample >= edges[b]) bin = b + 1;
}
out[bin] = out[bin] + 1;
}
This is the same rule as the Python golden — the bin is the number of edges a
sample meets or exceeds — written once, by hand, with the HLS pragmas that make it
synthesize well. hist_validate_impl.cpp and hist_respond_impl.tpp similarly
hold the bounds/alignment checks and the response write. They are committed; the
generated gen/ files that call them are not. (See
CODEGEN_NOTES.md for the full
generated-vs-hand-written breakdown.)
The generated testbench
tb_files_to_str(HistTBHls) lowers the testbench main() into gen/hist_tb.cpp.
It allocates the three regions in the same order the SimPy controller did, and —
because a runtime count can be 0 (nbins-1 edges when nbins == 1, or an
invalid size) — it clamps every allocation to at least one word:
// gen/hist_tb.cpp (one of three regions)
const int _cmd_data_addr_nwords = float32_array_utils::get_nwords<32>(cmd.ndata);
const int _cmd_data_addr_widx = mem_mgr.alloc(_cmd_data_addr_nwords > 0 ? _cmd_data_addr_nwords : 1);
cmd.data_addr = _cmd_data_addr_widx * (32 / 8);
float32_array_utils::write_array_slice<32>(data, mem + _cmd_data_addr_widx, 0, cmd.ndata);
A 0-word region is meaningless and the allocator rejects it, so the testbench
reserves one word it never uses — the same max(nedges, 1) clamp the SimPy
controller applied. The generated TB writes the kernel’s response and counts back
out as .bin files, which the C and RTL simulation flow checks
against the golden.
Running the codegen
The generation needs no Vitis — it is pure Python:
cd examples/shared_mem
python -c "from hist_build import generate_vitis_sources; generate_vitis_sources('.')"
ls gen/ # hist.cpp hist.hpp hist_tb.cpp
After it lands, inspect gen/hist.cpp and confirm the three hook .cpp/.tpp
files exist in the example directory.
Next
- C and RTL simulation — compiling the generated kernel + testbench with Vitis HLS, the four-case C-sim coverage, C-synthesis, RTL co-simulation, and the multi-buffer burst extraction.