Register Map

This page documents the AXI-Lite addressing model used across all axi-soc-ultra-plus-core platform designs. The same hierarchical crossbar pattern appears in every Simple-*-Example application repo; only the per-application sub-indices (documented in each application repo’s own reference pages) differ.

Hierarchical AXI-Lite crossbar

The Zynq UltraScale+ PS initiates all AXI-Lite register transactions. These are forwarded across the PS-to-PL boundary through the AxiSocUltraPlusCore block and dispatched by a two-level crossbar tree:

Top-level crossbar (inside the application’s top-level VHDL entity):

PS AXI-Lite master
     │
     ▼
AxiSocUltraPlusCore (AXI-Lite bridge)
     │
     ▼
Top-level AxiLiteCrossbar (3 slaves, 28-bit decode)
     ├─ [0] HW_INDEX_C   → AxiSocUltraPlusCore registers
     ├─ [1] RFDC_INDEX_C → RfDataConverter / RFDC IP registers
     └─ [2] APP_INDEX_C  → Application crossbar (second level)

Application-level crossbar (inside Application.vhd):

APP_INDEX_C slave
     │
     ▼
Application AxiLiteCrossbar (N slaves, 24-bit decode)
     ├─ [0] → AppRingBuffer registers
     └─ [1] → DacSigGen registers
     └─ ... (per-application; documented in each application repo)

The full address of any register is the sum of offsets along the path from the PS base address to the register’s leaf node. Python-side offsets in pr.Device subclasses must match the VHDL crossbar configuration exactly.

genAxiLiteConfig pattern

The surf library function genAxiLiteConfig generates the AXIL_CONFIG_C constant array that parameterizes every AxiLiteCrossbar instance:

-- Declare the number of slaves and index constants
constant NUM_AXIL_MASTERS_C : natural := 3;
constant HW_INDEX_C         : natural := 0;
constant RFDC_INDEX_C       : natural := 1;
constant APP_INDEX_C        : natural := 2;

-- Generate the crossbar configuration
constant AXIL_CONFIG_C : AxiLiteCrossbarMasterConfigArray(NUM_AXIL_MASTERS_C-1 downto 0) :=
    genAxiLiteConfig(NUM_AXIL_MASTERS_C, AXIL_BASE_ADDR_G, 28, 26);

Arguments to genAxiLiteConfig:

  • NUM_MASTERS — number of slave slots

  • BASE_ADDR — base address of the crossbar (passed in as a generic)

  • ADDR_BITS — total decode width in bits

  • DECODE_BITS — bits used to select among slaves (upper bits of the decode window)

Each slave’s base address is then AXIL_CONFIG_C(INDEX).baseAddr.

The Python device tree mirrors these addresses:

# Top-level offsets from the PS base address 0x04_0000_0000
# HW_INDEX_C=0  → AxiSocCore at offset 0x0000_0000
# RFDC_INDEX_C=1 → Rfdc device at offset determined by AXIL_CONFIG_C(1).baseAddr
# APP_INDEX_C=2  → Application device at offset 0xA000_0000

self.add(soc_core.AxiSocCore(
    offset = 0x0000_0000,
))
self.add(Application(
    offset = 0xA000_0000,
))

Signal initialization for undriven slaves uses the DECERR constant:

signal axilReadSlaves : AxiLiteReadSlaveArray(NUM_AXIL_MASTERS_C-1 downto 0) :=
    (others => AXI_LITE_READ_SLAVE_EMPTY_DECERR_C);
signal axilWriteSlaves : AxiLiteWriteSlaveArray(NUM_AXIL_MASTERS_C-1 downto 0) :=
    (others => AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C);

Any unmapped address returns DECERR rather than hanging the bus.

Slv256Array sample bus

ADC and DAC sample data between the RFDC wrapper and the application logic flows on a 256-bit parallel bus at dspClk (312.5 MHz). The type is defined in surf.StdRtlPkg:

-- 4 ADC channels, each 256 bits wide
dspAdc : in  Slv256Array(3 downto 0);
-- 2 DAC channels, each 256 bits wide
dspDac : out Slv256Array(1 downto 0);

Each 256-bit word carries SAMPLE_PER_CYCLE_C = 16 samples of 16 bits each:

  • 16 samples × 16 bits = 256 bits per channel per dspClk cycle

  • At 312.5 MHz, this yields an effective sample rate of 312.5 MHz × 16 = 5 GS/s throughput (the actual ADC/DAC sample rate is set by the RFDC tile configuration)

The Slv256Array type is generic across all channel counts. Each application’s top-level entity declares the port widths to match its board’s RFDC configuration.

AppPkg constants pattern

Each application repo contains a VHDL package AppPkg.vhd that declares the design-wide constants tying RTL generics to Python parameters:

package AppPkg is

   -- DMA lane count: lane 0 = ring buffer data, lane 1 = loopback debug
   constant DMA_SIZE_C : positive := 2;

   -- Samples per dspClk cycle — must match smplPerCycle in Python Application device
   constant SAMPLE_PER_CYCLE_C : positive := 16;

   -- AXI-Lite clock frequency (used by surf timing primitives)
   constant AXIL_CLK_FREQ_C : real := 100.0E+6;

end package AppPkg;

These constants are consumed by RTL generics throughout the design:

U_AppRingBuffer : entity axi_soc_ultra_plus_core.AppRingBuffer
    generic map (
        ADC_SAMPLE_PER_CYCLE_G => SAMPLE_PER_CYCLE_C,
        DAC_SAMPLE_PER_CYCLE_G => SAMPLE_PER_CYCLE_C,
        DMA_SIZE_G             => DMA_SIZE_C)
    port map (...);

The Python Application device must pass a matching value:

self.add(hardware.AppRingBuffer(
    numAdcCh     = NUM_ADC_CH_C,
    numDacCh     = NUM_DAC_CH_C,
    smplPerCycle = SAMPLE_PER_CYCLE_C,   # must equal AppPkg.SAMPLE_PER_CYCLE_C
))

Changing SAMPLE_PER_CYCLE_C requires synchronized updates in both the VHDL package and the Python device tree. The two values are not automatically cross-checked at build time.

External references