Endpoint interfaces

Most of the time you do not need this page. Declare an endpoint in Python and the mapping to Vitis ports is handled for you — codegen derives the kernel’s whole argument list and every #pragma HLS INTERFACE from your endpoints, and you never write one by hand.

You need it when you look inside the generated kernel. Chiefly: writing a custom hook, because the C++ arguments your hook receives are these ports — you cannot write the body without knowing that a stream endpoint arrives as an hls::stream<axi4s_word<bw>>& and an m_axi master as a plain pointer. Also when reading a synthesized block’s port list to integrate or debug it.

Concept

This page is organized per kind — stream, m_axi, regmap, bram. Where those kinds come from, and the fact that the XSI testbench reads the same vocabulary to pick its BFM models, is Endpoint kinds.

A generated kernel’s argument list and its #pragma HLS INTERFACE block are derived directly from the component’s declared endpoints by kernel_signature(comp). Each endpoint type maps to a specific Vitis port realization, emitted in a canonical order — streams, then regmap fields, then m_axi masters.

Endpoint (Python) Kernel argument Interface pragma
StreamIFMaster / StreamIFSlave hls::stream<streamutils::axi4s_word<bw>>& <name> #pragma HLS INTERFACE axis port=<name>
VitisRegMapMMIFSlave (per field) <cpp_type>& <field> (or <elem>[<count>] for a raw array field) #pragma HLS INTERFACE s_axilite port=<field> bundle=control
m_axi master (e.g. MMIFMaster / DirectMMIF master) ap_uint<bw>* <name> #pragma HLS INTERFACE m_axi port=<name> offset=slave bundle=gmem depth=<name>_depthplus s_axilite port=<name> bundle=control when the component has a regmap (see below)

Stream endpoints → axis

Every stream endpoint becomes an hls::stream reference of AXI4-Stream words, with an axis interface pragma. The word bitwidth is the endpoint’s concrete bitwidth (or the variant’s HwParamValue). The generated poly kernel:

void poly(
    hls::stream<streamutils::axi4s_word<32>>& s_in,
    hls::stream<streamutils::axi4s_word<32>>& m_out,
    ...

Master and slave streams both realize as hls::stream<...>& — the direction is a property of how the body reads or writes them, not of the port type.

Regmap slave → s_axilite + ap_ctrl

A VitisRegMapMMIFSlave does not become one port — it expands to one s_axilite port per user-declared register field, each on bundle=control. The generated simp_fun kernel turns the x / a / b / y register fields into four scalar references:

void simp_fun(
    ap_int<32>& x,
    ap_int<32>& a,
    ap_int<32>& b,
    ap_int<32>& y
);

The ap_start / ap_done control bits are not emitted as data ports; they are the control protocol. When a regmap drives the component, the kernel’s return port also binds s_axilite ... bundle=control, giving the host the ap_start/ap_done handshake that launches the regmap-launched on_start body.

m_axi master → m_axi pointer

A memory-mapped master endpoint becomes an ap_uint<bw>* pointer with an m_axi pragma (offset=slave bundle=gmem), the burst region bounded by a generated <name>_depth header constant. The generated hist kernel:

void hist(
    hls::stream<streamutils::axi4s_word<32>>& s_in,
    hls::stream<streamutils::axi4s_word<32>>& m_out,
    ap_uint<32>* m_mem
);

(Note the canonical order: the two streams precede the m_mem master.) Reading and writing through this pointer inside the body — the lane/slice transactions — is Custom Hooks material.

offset=slave needs a home for the pointer

offset=slave means the pointer’s base address is not a port — it arrives in an AXI-Lite register that the host writes before launching. But bundle=gmem names the m_axi bundle; it says nothing about where that offset register lives. So when the component has a regmap, codegen also emits

#pragma HLS INTERFACE s_axilite port=m_mem  bundle=control

binding the offset register into the same control slave as ap_start/ap_done. Without it Vitis silently invents a second AXI-Lite bundle for the offset alone, and the kernel exposes two address spaces (s_axi_control and an auto-named s_axi_control_r) — one block, two slaves, for no reason.

A component with an m_axi but no regmap still has this problem. There is no control bundle for the offset to join, so Vitis auto-creates one and ap_start stays on raw pins — meaning the block needs two different masters: one to write the base address over AXI-Lite, another to pulse a wire. block_scale is in this state today. hist was, until it gained a regmap and became HostActivated. This is a good reason to give a memory-mapped kernel a regmap even when it has no scalar arguments to put in one.

The control protocol on return

ap_ctrl_hs is the protocolap_start / ap_done / ap_idle / ap_ready. Which pragma binds the return port decides only how those signals are reached, and follows from the endpoint mix (in kernel_signature):

Endpoint mix return binds What the RTL exposes
a regmap slave is present s_axilite ... bundle=control ap_start is not a port — Vitis generates a <kernel>_control_s_axi adapter that drives it from register 0x00
else m_axi masters present ap_ctrl_hs ap_start/ap_done/ap_idle/ap_ready are top-level pins, driven by whatever instantiates the block
else (stream-only) s_axilite ... bundle=control as the first row

The first row is worth dwelling on, because it explains a name: s_axilite port=return does not give you a different kernel — it gives you the same pin-driven core plus a generated AXI-Lite→pin adapter. That adapter is a real, separate RTL module, and on_start is its callback — which is why on_start exists exactly when a regmap does.

Neither row is “better”: a host-launched accelerator (XRT) needs the AXI-Lite control registers, while a block launched by another block inside an IPI system wants the raw pins. See Hardware modules and Flows.

ap_ctrl_none — free-running, no handshake at all — is a third protocol that nothing generates yet (the free_running_kernel target). Note a component whose entry is run_proc rather than on_start is not free-running; it is ap_ctrl_hs on raw pins, per the middle row.

How a slave endpoint’s handler binds

A slave endpoint carries the Python handler that becomes (part of) the synthesized body:

  • A VitisRegMapMMIFSlave is constructed with on_start=self.on_start (see examples/regmap/simp_fun.py). That handler is exactly the method extract_kernel lowers as the kernel body — the regmap slave both adds the s_axilite ports and designates the ap_start-triggered entry point.
  • For stream / m_axi endpoints there is no separate handler method: the free-running run_proc body reads and writes them directly, and those per-port read/write calls are lowered to hls::stream / m_axi transactions. The transaction methods themselves (read_stream_lane, read_array_lane, …) are documented in Custom Hooks: Kernel transfer reference.

API

Quick reference

  • Argument order is canonical: streams → regmap fields → m_axi masters.
  • Stream endpoint ⇒ hls::stream<axi4s_word<bw>>& + axis.
  • Regmap slave ⇒ one s_axilite port per field + ap_start/ap_done control; its on_start is the kernel body.
  • m_axi master ⇒ ap_uint<bw>* + m_axi offset=slave bundle=gmem depth=<name>_depth.
  • return protocol: s_axilite when a regmap (or stream-only) drives control, ap_ctrl_hs for m_axi-only kernels.