A high-performance FPGA shell built on the Xilinx QDMA subsystem for Alveo boards. It provides PCIe 4.0 host DMA, 100 GbE CMAC networking, and a Dynamic Function eXchange (DFX) reconfigurable partition (RP) for user logic, so the shell stays static while the user module is reloaded at run time.
This document covers the parts that are common to every board. Board-specific details (floorplan, clocking, timing-closure notes, build status) live in each board's own README — see Supported boards.
- Top level (
src/shell_top.v) — integrates PCIe, Ethernet, and memory. - QDMA subsystem (
src/qdma_wrapper.v) — PCIe 4.0 x8 + host↔FPGA DMA. - Ethernet subsystem (
src/cmac_subsystem.v) — wraps the 100 GbE CMAC(s). - User block (
src/user_block.sv) — the user-logic interface, split into:- Static part (
bd_user) — Vivado block design with the NIC datapath (packet routers, switches, FIFOs) and infrastructure. - Reconfigurable partition (
rp_blk) — the DFX partition holding user logic; ships with a default loopback module.
- Static part (
- Host interfaces — AXI-Lite (control/config), AXI-MM (memory-mapped access), and AXI-Stream over QDMA (host packet DMA) and CMAC (network).
| Path | Contents |
|---|---|
boards/ |
Per-board params (params.tcl), constraints (base.xdc, rp.xdc), block-design scripts (bd_user.tcl, rp_user.tcl), and a board README. |
src/ |
Verilog / SystemVerilog RTL. |
ips/ |
Custom IP repository. The active packet router is pktrte_dual (AxisPacketRouterDual); pktrte_mono is the superseded single-output variant. |
script/ |
Shared TCL for the build flow and IP configuration (e.g. conf_ip.tcl, abs_flow.tcl) and the check_axi.py linter. |
build/ |
Generated Vivado projects (one subdir per PROJ). |
Makefile, build.tcl, build_module.tcl |
Build entry points. |
The flow is driven by a Makefile plus Vivado TCL scripts.
| Variable | Default | Meaning |
|---|---|---|
BOARD |
au50 |
Target board (au50, au280, au55c). |
PROJ |
<BOARD>_shell |
Vivado project name (build dir = build/<PROJ>). |
PRFLOW |
0 |
1 enables the Partial Reconfiguration (DFX) flow. |
RUN |
0 |
1 runs synth + impl; 0 only creates the project. |
NTHRD |
16 |
Parallel Vivado jobs (forced to 0 when RUN=0). |
Source the matching Vivado before building — see the board table for the required version.
make shell BOARD=<board> PROJ=<name> PRFLOW=0 # PRFLOW=0 is the defaultGenerates the Vivado project and, with RUN=1, builds the full monolithic
shell.
Monolithic UUID sentinel. In a non-PR build the low 32-bit word of the build-info UUID (word 3 — the last 8 hex characters of the canonical
xxxxxxxx-xxxx-xxxx-xxxx-xxxxFFFFFFFFstring) is forced to0xFFFFFFFF. This marks the loaded image as a monolithic application that does not support partial reconfiguration. A PR-flow (PRFLOW=1) shell keeps a fully random low word, so a host reading the infopage can distinguish a monolithic image from a reconfigurable shell by the UUID alone.
- Shell:
make shell BOARD=<board> PROJ=<name> PRFLOW=1
- Reconfigurable module (
rp_blk):Produces a synthesis checkpoint (make module BOARD=<board> PROJ=<module_name>
.dcp) for the RP. - Link (manual): use the helpers in
script/abs_flow.tclto link the RP.dcpinto the PR-enabled shell and emit the final bitstreams. This step is not yet wired into theMakefile.
The QDMA exposes one AXI-Lite master (BAR2). It fans out through the top-level
axil_xbar (configured in script/conf_ip.tcl) to six master ports. This
map is shared by every board.
| M-port | Range | Size | Destination |
|---|---|---|---|
| M00 | 0x000000–0x1FFFFF |
2 MB | RP partition (rp_user_inst) — rpen-gated |
| M01 | 0x200000–0x2FFFFF |
1 MB | Static shell (bd_user_inst.s_axil_cfg) |
| M02 | 0x310000–0x31FFFF |
64 KB | CMAC0 (QSFP0) |
| M03 | 0x320000–0x32FFFF |
64 KB | CMAC1 (QSFP1) |
| M04 | 0x308000–0x30FFFF |
32 KB | System Management (sys_mgmt_wiz) |
| M05 | 0x300000–0x307FFF |
32 KB | QDMA CSR |
Within the 1 MB window at base 0x200000, the BD SmartConnect axil_sc_i
decodes to:
| Absolute | Sub-offset | Block (BD path) |
|---|---|---|
0x208000–0x208FFF |
0x8000 |
pkt_route_0/s_axil/reg0 (AxisPacketRouterDual, CMAC0) |
0x209000–0x209FFF |
0x9000 |
pkt_route_1/s_axil/reg0 (AxisPacketRouterDual, CMAC1) — dual-CMAC boards only |
0x20C000–0x20CFFF |
0xC000 |
h2c_dst_trans_i/s_axil/reg0 (AxisDestTrans) |
0x20E000–0x20EFFF |
0xE000 |
pr_subsys/axi_gpio_prctrl/S_AXI (PR-control GPIO) |
0x20F000–0x20FFFF |
0xF000 |
pr_subsys/axi_hwicap/S_AXI_LITE (ICAP) |
Single-CMAC boards (au50) have one router and omit 0x209000
(axil_sc_i NUM_MI=4); dual-CMAC boards (au280, au55c) include it
(NUM_MI=5). The PR-control GPIO drives rp_reset / rp_detach.
The RP partition's s_axil is reached directly through xbar M00 and is gated
by rpen (so a detached/blank RP cannot stall the bus):
| Absolute | Sub-offset | Size | Block |
|---|---|---|---|
0x000000–0x07FFFF |
0x00000 |
512 KB | axi_gpio_0/S_AXI/Reg |
0x080000–0x1FFFFF |
— | 1.5 MB | unused (RP-side reserve) |
The RP also exposes AXI-MM spaces (s_axi_dma, s_axi_pcie) to axi_bram_ctrl
and m_axibr; those are not reached through the top axil_xbar and are
listed in each board's rp_user.tcl for reference only.
src/axil_xbar_cdc.vmasks each CDC input address to its window width so downstream blocks see 0-based offsets; software still uses the absolute addresses above.
The static block design implements the NIC datapath between the CMAC(s), the
host (QDMA), and the RP. Each CMAC has its own dual-output packet router
(pkt_route_N); a host-side splitter (h2c_sw) and an egress switch (nicsw)
steer traffic by AXI-Stream tdest. The diagram shows a dual-CMAC board:
eth_rx_0 ─► pkt_route_0 m_axis_0 ─► nicsw.S03 m_axis_1 ─► ethrx0 ─► RP
eth_rx_1 ─► pkt_route_1 m_axis_0 ─► nicsw.S04 m_axis_1 ─► ethrx1 ─► RP
QDMA H2C ─► fifo ─► h2c_dst_trans_i ─► h2c_sw
├─ tdest 0xFFF0–0xFFFF ─► nicsw.S00
└─ tdest 0x0000–0xFFEF ─► rph2c ─► RP
RP ─► rpout0 ─► nicsw.S01 RP ─► rpout1 ─► nicsw.S02
nicsw routes by tdest:
0x0000–0xFFEF ─► QDMA C2H (to host)
0xFFF0 ─► eth_tx_0 (CMAC0 wire)
0xFFF1 ─► eth_tx_1 (CMAC1 wire)
The RP boundary is 3 streams in (rph2c, ethrx0, ethrx1) and
2 streams out (rpout0, rpout1). On single-CMAC boards (au50) there is
no pkt_route_1, ethrx1, or eth_tx_1, and 0xFFF1 is unused.
| tdest value | Destination |
|---|---|
0x0000–0xFFEF |
Host (QDMA C2H) |
0xFFF0 |
CMAC0 TX (eth_tx_0) |
0xFFF1 |
CMAC1 TX (eth_tx_1) |
- Receive (wire → host): each
pkt_route_N(programmed at0x208000/0x209000) by default forwards received frames outm_axis_0with a host-range tdest, sonicswdelivers them to QDMA C2H; the host reads them off its C2H queues. Frames steered outm_axis_1go into the RP (ethrxN) for user-logic processing instead. - Transmit (host → wire): the host writes a packet through a QDMA H2C
queue;
h2c_dst_trans_i(0x20C000) stamps the AXIStdestfrom the descriptor.tdest 0xFFF0reaches CMAC0,0xFFF1reaches CMAC1;≤0xFFEFis delivered into the RP viarph2c.
RX-to-host and TX-from-host do not require an RP to be loaded — the rpen
gating keeps a detached/blank RP from stalling these paths. The default
loopback RP merges its three inputs into one FIFO and splits back to
rpout0/rpout1 by tdest bit 15.
The tdest-steering blocks are custom BSV IPs configured over AXI-Lite. Their
register models are summarised here so software can drive them directly. All
registers are 32-bit, little-endian, word-aligned; offsets are relative to each
block's window (see the address maps above). BSV source lives in the
mds_ethpl repo (src/AxisPacketRouter.bsv, src/AxisDestTrans.bsv + .md).
A header match-action router: it matches the first beat of each packet
against a table, then for the winning entry replaces the outgoing tdest
and selects which downstream master (m_axis_0 / m_axis_1) the packet leaves
on. The packed variant (pktrte_dual, mkAxisPacketRouterCmac27) is 512-bit
data, 2 downstream ports, 8 entries (entry 0 + 7 match entries).
| Offset | Access | Layout |
|---|---|---|
0x000 |
R / W | R: IDENT [31:8]=0xC32956, [7:4]=#downstreams, [3:0]=#entries. W any non-zero = soft reset |
0x004 |
R / W | force ctrl: [4]=force_drop (drop all), [0]=force_match enable, [11:8]=forced entry id |
0x008 / 0x00C |
R | upstream packet count, low / high 32 bits |
0x010 + 0x10·e |
R / W | entry e action word0: [3:0]=downstream port, [31:16]=TID reload |
0x014 + 0x10·e |
R / W | entry e action word1: [15:0]=egress tdest (overwrites the beat), [31:16]=metadata |
0x018 / 0x01C + 0x10·e |
R | entry e matched-packet count, low / high |
0x100 + 0x80·(e-1) |
R / W | entry e (≥1) 512-bit header match value, [5:2]=32-bit word index (0–15) |
0x140 + 0x80·(e-1) |
R / W | entry e (≥1) 512-bit mask, [5:2]=word index |
Match semantics: a beat matches entry e when (data & mask) == (header & mask)
over the kept bytes. Entry 0 is the miss/default action (no header) used only
when nothing else matches; entries 1…N-1 are header entries, lowest index
wins.
Reset quirk: entries reset to
{port 0, tdest 0, mask 0, header 0}, and a zero mask matches every packet. So at reset entry 1 (lowest header entry) catches all traffic and forwards it to downstream port 0 withtdest 0. To change the default behaviour you must reprogram entry 1, not entry 0 — writing only entry 0 has no effect while a maskless entry 1 still matches everything.
An inline tdest translator (ternary CAM). It passes every beat through
unchanged except tdest, which is rewritten from the first beat's value and
applied to the whole packet. The packed variant (mkAxisDestTrans15) has
15 entries.
| Offset | Access | Layout |
|---|---|---|
0x000 |
R | IDENT [31:8]=id prefix, [7:0]=entry count |
0x004 |
R / W | DEFAULT_TRANS (miss action): [31:16]=mask, [15:0]=value |
0x008 + 8·i |
R / W | ENT_MATCH[i]: [31:16]=match_mask, [15:0]=match_value |
0x00C + 8·i |
R / W | ENT_TRANS[i]: [31:16]=trans_mask, [15:0]=trans_value |
An entry is enabled only when both its match_mask and trans_mask are
non-zero; lowest index wins; on a miss DEFAULT_TRANS applies. The result is
tdest_out = (tdest_in & ~trans_mask) | (trans_value & trans_mask). Reset is
all-zero → identity passthrough (transparent wire).
The RP's internal
AxisPacketRouterDual_0is not on the AXI-Lite xbar — it lives in the QDMA AXI-MM space at0x08200000, reached through the PCIe-bridge BAR (s_axi_pcie) or an MM DMA (s_axi_dma), per each board'srp_user.tcl. au280's stock RP has no such router: its egress is a fixedrp_out_swaxis_switchthat splits bytdestbit 15 and cannot rewritetdest, so the stock au280 RP can only return host-range traffic to C2H — it cannot turn an RP-range packet into a0xFFF0/0xFFF1wire-egress one. au50/au55c can, because their RP egress is the programmableAxisPacketRouterDual_0.
| Board | Part | CMAC / QSFP | Memory | Vivado | README |
|---|---|---|---|---|---|
| au50 | xcu50-fsvh2104-2-e |
1 × 100 GbE | HBM | 2024.2 | boards/au50/README.md |
| au280 | xcu280-fsvh2892-2L-e |
2 × 100 GbE | DDR4 | 2023.2 | boards/au280/README.md |
| au55c | xcu55c-fsvh2892-2L-e |
2 × 100 GbE | HBM | 2024.2 | boards/au55c/README.md |
au280's part is absent from Vivado 2024.2 — build it with 2023.2.
All AXI interfaces follow standard AXI4 / AXI4-Lite / AXI-Stream directional
conventions. script/check_axi.py enforces this across the primary RTL files
(m_ = master, s_ = slave) and checks cross-module consistency between
user_block.sv and rp_blk.v:
python3 script/check_axi.py # PASS/FAIL only
python3 script/check_axi.py -v # include intentional differencesKnown-intentional interface mismatches are documented in AGENTS.md and must
not be "fixed".
A release ships pre-built bitstreams as zipped packages, produced by the
packagers in script/:
| Package | Built by | Contents |
|---|---|---|
Shell package (<name>_support_<uuid8>.zip) |
mk_support_pkg.py |
Full shell bitstream (shell_top.bit/.ltx) plus the pieces needed to build an app against it (Vivado part, rp_blk.v, board_config.vh, abstract-shell .dcp). |
App package (<name>_app_<uuid8>.zip) |
mk_app_pkg.py |
The RP partial bitstream(s) and a manifest. --rp-only packs the partial bitstream only (no full shell bitstream) — the artifact to load onto an already-running shell. |
Both packages are tagged with the shell's 8-character UUID (<uuid8>). The host
reads the running shell's UUID from the info page (see the monolithic-UUID note
under Building) and an app package's manifest records the shell
UUID it was built against, so a partial bitstream can be matched to its shell in
the field and a mismatched app is refused.
To build these from a finished Vivado project, see the packaged-shell app flow
in AGENTS.md.
Licensed under the Apache License, Version 2.0 — see LICENSE.
This shell borrows and derives from the Xilinx OpenNIC
Shell, which is also Apache-2.0;
those files under src/ keep their original Xilinx headers. src/crc32.v is
third-party (OutputLogic.com) under its own permissive terms. Borrowed sources
remain under their original licenses; see NOTICE for details.