AIE-ML Moving Average Crossover kernel for the VD100 (XCVE2302).
Part of the vd100-aie-pipeline Vitis system project.
This component implements a dual moving-average crossover trading signal detector running entirely on the AIE-ML array of the Versal AI Edge VE2302 (VD100 board).
Price ticks stream into the AIE graph via an HLS mm2s DMA kernel. Each iteration the graph outputs three int32 values — fast MA, slow MA, and a BUY/SELL/HOLD signal — which are collected by an HLS s2mm DMA kernel into PS DDR for the host application to read.
PS DDR (int32[])
│
▼
mm2s (HLS) ──AXI4-Stream──► AIE mygraph (ma_crossover)
│
AXI4-Stream▼
s2mm (HLS) ──► PS DDR (int32[])
Each AIE iteration receives BLOCK_SIZE=56 new int32 price samples.
An ADF margin of SLOW_MA_PERIOD=50 samples carries history from the previous
iteration, giving the kernel 106 samples total to work with.
| Constant | Value | Description |
|---|---|---|
FAST_MA_PERIOD |
10 | Fast moving average window (periods) |
SLOW_MA_PERIOD |
50 | Slow moving average window (periods) |
BLOCK_SIZE |
56 | int32 input samples per AIE iteration |
NUM_MARGIN_SAMPLES |
56 | ADF margin size — must equal BLOCK_SIZE |
| Output per block | 3 | { fast_ma, slow_ma, signal } |
Signal encoding:
| Value | Meaning |
|---|---|
1 |
BUY — fast MA crossed above slow MA |
-1 |
SELL — fast MA crossed below slow MA |
0 |
HOLD — no crossover |
Crossover detection uses previous-vs-current MA comparison:
BUY: prev_fast_ma <= prev_slow_ma AND fast_ma > slow_ma
SELL: prev_fast_ma >= prev_slow_ma AND fast_ma < slow_ma
HOLD: otherwise
ptr[0 .. 49] = margin (50 samples — history from previous iteration)
ptr[50 .. 105] = new samples this iteration (56 samples)
total = 106 samples available to kernel
vd100-aie-ma-crossover/
├── src/
│ ├── kernels/
│ │ ├── kernels.cc # AIE kernel: ma_crossover<M> template
│ │ └── include.h # Constants: FAST_MA_PERIOD, SLOW_MA_PERIOD, BLOCK_SIZE
│ ├── project.h # MAGraph class (ADF graph definition)
│ ├── project.cpp # Graph instantiation: MAGraph mygraph; main() for sim
│ ├── kernels.h # Kernel function declarations
│ ├── mm2s.cpp # HLS PL kernel: PS DDR → AXI4-Stream → AIE
│ └── s2mm.cpp # HLS PL kernel: AIE → AXI4-Stream → PS DDR
├── data/
│ ├── input.txt # Golden test input (4 blocks × 56 samples)
│ └── golden.txt # Expected output for simulation verification
├── aiecompiler.cfg # AIE compiler configuration
├── CMakeLists.txt # Build config (for Vitis system project integration)
└── vitis-comp.json # Vitis component descriptor
// mm2s — streams from PS DDR into AIE
void mm2s(ap_int<32>* mem, hls::stream<ap_axis<32,0,0,0>>& s, int size);
// s2mm — collects AIE output into PS DDR
void s2mm(ap_int<32>* mem, hls::stream<ap_axis<32,0,0,0>>& s, int size);Both kernels: mem = arg[0] → group_id(0) for XRT BO allocation.
size is in bytes (not element count) — the host must pass count * sizeof(int32_t).
This component is built as part of the vd100_ma_system_project Vitis system project,
not standalone. The system project handles:
- AIE compilation (
v++ --compile --target hw) - HLS kernel synthesis (
v++ --compile --target hw) - Link (platform + AIE + HLS kernels →
aie.xclbin) - Package →
BOOT.BINcomponents
v++ --compile \
--target hw \
--platform <path_to_vd100_platform>/vd100_platform.xpfm \
--config aiecompiler.cfg \
-o aie.o \
src/project.cppThe project.cpp main() function runs 4 iterations using data/input.txt.
Compare output against data/golden.txt to verify correctness:
# x86 simulation (fast, no hardware needed)
v++ --compile --target x86sim ...
# or via Vitis IDE: Run → AIE SimulationThis is the most important undocumented gotcha for Versal AIE under Linux.
The Yocto xilinx-bootbin recipe by default generates a BOOT.BIN that does not
include the AIE partition initialisation CDOs. Without them, PLM has no AIE data to
process at boot and all tiles remain clock_gated permanently. XRT then fails silently
in an infinite retry loop when graph.run() is called.
# All tiles clock_gated — XRT hammers ioctl, app hangs
cat /sys/class/aie/aieaperture_0_17/aiepart_0_17/core
0_2: clock_gated
...
# dmesg floods with:
aie aiepart_0_17: Tile(7,2) is gated. Failed to write to 0xe200004
The v++ package step generates two CDO files that must be in BOOT.BIN:
| Partition | File | Purpose |
|---|---|---|
aie_dev_part |
libadf/sw/aie.cdo.device.partition.reset.bin |
AIE partition device init |
aie_image |
aie.merged.cdo.bin |
Tile init, clock enable, ELF load |
Without aie.merged.cdo.bin: PLM never enables AIE column clocks → tiles stay gated.
Add to meta-vd100_v3/recipes-bsp/bootbin/xilinx-bootbin_1.0.bbappend:
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
SRC_URI += "file://aie.cdo.device.partition.reset.bin \
file://aie.merged.cdo.bin"
python do_configure:append() {
workdir = d.getVar('WORKDIR')
b = d.getVar('B')
for f in ['aie.cdo.device.partition.reset.bin', 'aie.merged.cdo.bin']:
src = os.path.join(workdir, f)
dst = os.path.join(b, f)
shutil.copyfile(src, dst)
bb.note('VD100 AIE: copied %s -> %s' % (src, dst))
fp = d.getVar('BIF_FILE_PATH')
with open(fp, 'r') as fh:
content = fh.read()
content = content.rstrip()
if content.endswith('}'):
content = content[:-1]
content += """
\timage {
\t\tname=aie_dev_part, id=0x18800000
\t\t{ type=cdo, file=aie.cdo.device.partition.reset.bin }
\t}
\timage {
\t\tname=aie_image, id=0x18800000
\t\t{ type=cdo, file=aie.merged.cdo.bin }
\t}
}"""
with open(fp, 'w') as fh:
fh.write(content)
bb.note('VD100 AIE: BIF updated with aie_dev_part and aie_image')
}Copy the CDO files from the v++ package output:
# Source locations after v++ package
libadf/sw/aie.cdo.device.partition.reset.bin
package/aie.merged.cdo.bin
# Destination
meta-vd100_v3/recipes-bsp/bootbin/files/aie.cdo.device.partition.reset.bin
meta-vd100_v3/recipes-bsp/bootbin/files/aie.merged.cdo.binNote:
osandshutilimports are not needed in the bbappend — they are already in scope from the parent recipe'sdo_configure. Addingimport oswill causeUnboundLocalErrordue to Python/BitBake scoping behaviour.
bootgen -read BOOT.BIN -arch versal 2>/dev/null | grep -E "Image:|aie"Expected output:
+---Image: aie2_subsys [id:0x421c028]
+---Image: aie_dev_part [id:0x18800000]
+---Image: aie_image [id:0x18800000]
The zocl kernel driver must be configured with 32 IRQs from the AXI interrupt controller, not 4 hardcoded GIC SPI interrupts.
Generate the correct DT node using sdtgen:
sdtgen set_dt_param -dir sdt_out -zocl enableWithout this, zocl probes with wrong IRQ configuration, hardware state is reported
incorrectly (tiles show reset instead of clock_gated), and AXI errors cause
kernel panics instead of returning EINVAL.
data/input.txt — 4 blocks × 56 samples:
| Block | Content | Expected Output |
|---|---|---|
| 1 | 56 × 5000 | fast=5000, slow=5000, HOLD |
| 2 | 56 × 4990 | fast=4990, slow=4990, HOLD |
| 3 | 55 × 4990 + 1 × 5600 | fast=5051, slow=5002, BUY |
| 4 | 56 × 5600 | fast=5600, slow=5600, HOLD |
Block 3 triggers the crossover: a single 5600 spike pushes the fast MA above slow MA.
| Item | Value |
|---|---|
| Board | VD100 (XCVE2302-SFVA784-1LP-E-S) |
| AIE tile | col=8, row=0 (shim row) |
| PL clock | 100 MHz |
| Timing closure | WNS 4.217ns, WHS 0.018ns |
| XRT version | 2025.2 |
| Vitis version | 2025.2 |
| Kernel | 6.12.40-xilinx (stock, no patches) |
- BOOT.BIN CDO gap — the most impactful undocumented issue. No error is raised;
tiles silently remain clock_gated. Always verify with
bootgen -read. - No kernel patches required — stock kernel + correct zocl DT node + correct BOOT.BIN CDOs = working pipeline. All prior kernel workarounds were symptoms of the missing CDOs.
- JTAG interference — connecting Vitis JTAG while the XRT application is running causes the AIE array to be partially reinitialised, producing zero output. Always disconnect JTAG before running the PS host application.
sdtgen -zocl enableis mandatory — without it, zocl uses the wrong IRQ configuration and reports incorrect hardware state.- ADF vs AIE Graph API —
graph.run()(no args) is ADF;graph.run(N)is AIE Graph API. Do not mix. Usegraph.run(-1)for indefinite iterations.