Skip to content
RTL Design Sherpa CocoTB Framework · Verification Infrastructure for RTL Testing
GitHub · Documentation Index · MIT License

AXIL4 Components Overview

AXI4-Lite is what you reach for when the bus only needs to move registers: no bursts, no IDs, no sideband signals, one beat per transaction. The AXIL4 components in CocoTBFramework are built on the same GAXI substrate as the full AXI4 BFMs, so field configuration, memory models, statistics, and debug tooling all carry over -- what's gone is everything the Lite spec throws away.

Framework Integration

GAXI Infrastructure Foundation

AXIL4 isn't a separate stack; it's a specialization layered on GAXI. That buys you:

Unified Field Configuration: the same field configuration system the rest of the framework uses, trimmed to Lite's smaller field sets Memory Model Support: slaves can be backed by the shared memory and register models for real read-after-write behavior Statistics Integration: transaction counts and performance metrics come straight from the GAXI monitors Signal Resolution: automatic signal detection and mapping across different naming conventions Advanced Debugging: multi-level debug with detailed transaction logging

AXI4-Lite Protocol Specialization

On top of that foundation, these components are shaped specifically for Lite:

Simplified Five Channel Architecture: AR, R, AW, W, and B, with none of the burst bookkeeping Single-Beat Transfer Model: no burst support -- one beat per transaction. Multiple transactions may be outstanding at once; with no IDs, they simply complete in issue order Register-Oriented Design: the API is built around control/status register access patterns Reduced Signaling: no ID, USER, QoS, or REGION signals -- Lite doesn't have them Protocol Compliance: an integrated checker scoped to the Lite subset of the AXI4 rules

Core Components Architecture

graph TB
    subgraph Ecosystem["AXIL4 Component Ecosystem"]
        subgraph Components["Core Components - Single Transaction"]
            MstrRd["AXIL4MstrRd<br/>(AR/R)"]
            MstrWr["AXIL4MstrWr<br/>(AW/W/B)"]
            SlvRd["AXIL4SlvRd<br/>(AR/R)"]
            SlvWr["AXIL4SlvWr<br/>(AW/W/B)"]
        end

        subgraph FieldCfg["AXIL4 Field Configs - Simplified"]
            AR["AR Config<br/>(No ID/User)"]
            R["R Config<br/>(No ID/User)"]
            AW["AW Config<br/>(No ID/User)"]
            W["W Config<br/>(No User)"]
            B["B Config<br/>(No ID/User)"]
        end

        subgraph Features["AXIL4 Specific Features"]
            RegAPI[Register API]
            Single[Single-Beat Transfers]
            Compliance[Compliance]
            PktUtils[Packet Utils]
            Timing[Simplified Timing]
            Factories[Factories]
        end

        subgraph GAXI["GAXI Infrastructure"]
            SigRes[Signal Resolution]
            MemMdl[Memory Models]
            Stats[Statistics]
            Fields[Field Handling]
            Debug[Debug Support]
            Config[Configuration]
        end
    end

    Components --> FieldCfg
    FieldCfg --> Features
    Features --> GAXI

Component Capabilities

AXIL4MasterRead - Register Read Operations

The read master drives AR and listens on R. On the address side it manages ARADDR and ARPROT -- and that's the whole list, since there's no ARID, ARLEN, ARSIZE, or ARBURST to worry about. Addresses are aligned automatically, and the single-transaction model keeps timing simple.

On the data side it processes RDATA and RRESP (again: no RID, no RLAST), raises on SLVERR and DECERR, and manages RREADY so the DUT can apply backpressure. Both 32-bit and 64-bit data widths are supported.

Register-Oriented API:

# Register access methods
data = await master_read.read_register(address=0x100)
data = await master_read.single_read(address=0x200)  # API consistency
values = await master_read.read_transaction(address=0x300)  # Generic method

AXIL4MasterWrite - Register Write Operations

The write master keeps the two outgoing phases in order and checks the response. AW carries AWADDR/AWPROT, W carries WDATA/WSTRB -- no IDs, no AWLEN, no WLAST. WSTRB gives you byte-lane control over the write, and the B channel response is verified the same way RRESP is on the read side, with SLVERR/DECERR surfaced as errors.

Register-Oriented API:

# Register write methods
await master_write.write_register(address=0x100, data=0x12345678)
await master_write.write_register(address=0x100, data=0xFF, strb=0x1)  # Byte write
await master_write.single_write(address=0x200, data=0xDEADBEEF)  # API consistency

AXIL4SlaveRead - Register Read Response

The read slave watches AR, decodes the address against a configurable address range, handles ARPROT, and answers on R. With no bursts to sequence, the decode logic stays genuinely simple. Response data can come straight from a register or memory model, SLVERR/DECERR generation is configurable, and RVALID timing can be delayed to model a slow register block.

Memory Model Integration:

from CocoTBFramework.components.shared.memory_model import MemoryModel

# Back the slave with a memory model; reads are served from it
memory = MemoryModel(num_lines=1024, bytes_per_line=4)
slave_read = AXIL4SlaveRead(dut, clk, "s_axil_", memory_model=memory)

AXIL4SlaveWrite - Register Write Response

The write slave coordinates the AW and W phases -- both must land before it responds -- applies WSTRB per byte, updates the backing register or memory model, and drives B with the result. Address-based write protection and configurable response latency are available.

Advanced Features:

# Observe write traffic via the underlying channel callbacks
def on_w_packet(w_packet):
    print(f"W data=0x{w_packet.data:08X} strb=0x{w_packet.strb:X}")

slave_write.w_channel.add_callback(on_w_packet)

Field Configuration System

AXIL4FieldConfigs - Simplified Channel Configuration

The field configs are where the Lite diet shows. Each channel's config contains exactly the fields the spec allows, and the helper builds them for you:

Simplified Channel Configurations:

# AR Channel Configuration (no ID, LEN, SIZE, BURST)
ar_config = AXIL4FieldConfigHelper.create_ar_field_config(
    addr_width=32
)

# AW Channel Configuration (no ID, LEN, SIZE, BURST)
aw_config = AXIL4FieldConfigHelper.create_aw_field_config(
    addr_width=32
)

# R Channel Configuration (no ID, LAST)
r_config = AXIL4FieldConfigHelper.create_r_field_config(
    data_width=32
)

# W Channel Configuration (no ID, LAST)
w_config = AXIL4FieldConfigHelper.create_w_field_config(
    data_width=32
)

# B Channel Configuration (no ID)
b_config = AXIL4FieldConfigHelper.create_b_field_config()

The omissions are the point:

  • No ID Fields: the Lite spec omits them; with no IDs, any outstanding transactions simply complete in the order they were issued
  • No Burst Fields: AWLEN, ARLEN, AWSIZE, ARSIZE, AWBURST, ARBURST simply don't exist here
  • No USER Fields: Lite has no sideband signaling
  • No LAST Fields: one beat per transaction makes WLAST and RLAST meaningless

Advanced Features

AXIL4ComplianceChecker - Simplified Protocol Verification

The compliance checker covers the rules that survive in Lite -- VALID/READY handshake timing, payload stability, address alignment, write-strobe validity, response-code range -- and skips the ones that don't. No burst checking, no ID tracking: there are no bursts and no IDs. Concurrent read and write activity is legal in AXI4-Lite, so outstanding depth is reported as a statistic rather than flagged as a violation.

Register Model Integration

AXIL4 slaves pair naturally with a register map. A small definition class is enough to get readable names and access policies:

Register Definition:

class RegisterDef:
    def __init__(self, name, width, reset=0, readonly=False, writeonly=False):
        self.name = name
        self.width = width
        self.reset = reset
        self.readonly = readonly
        self.writeonly = writeonly
        self.current_value = reset

# Create register map
register_map = {
    0x000: RegisterDef("DEVICE_ID", 32, reset=0x12345678, readonly=True),
    0x004: RegisterDef("CONTROL", 32, reset=0x00000000),
    0x008: RegisterDef("STATUS", 32, reset=0x00000001, readonly=True),
    0x00C: RegisterDef("DATA_IN", 32, reset=0x00000000, writeonly=True),
    0x010: RegisterDef("DATA_OUT", 32, reset=0x00000000, readonly=True)
}

Register Access Monitoring:

def register_access_monitor(address, data, is_write, strobe=None):
    reg_name = register_map[address].name if address in register_map else "UNKNOWN"
    operation = "WRITE" if is_write else "READ"
    strobe_info = f" (strobe=0x{strobe:X})" if is_write and strobe is not None else ""
    print(f"Register {operation}: {reg_name} @ 0x{address:03X} = 0x{data:08X}{strobe_info}")

Usage Patterns and Integration

Basic Register Access

The common case: two masters, full-word and byte-lane access.

# Create AXIL4 master interfaces
master_read = AXIL4MasterRead(dut, clk, "m_axil_", data_width=32, addr_width=32)
master_write = AXIL4MasterWrite(dut, clk, "m_axil_", data_width=32, addr_width=32)

# Basic register operations
await master_write.write_register(0x100, 0x12345678)  # Write control register
status = await master_read.read_register(0x104)       # Read status register

# Byte-level operations
await master_write.write_register(0x108, 0xFF, strb=0x1)  # Write byte 0 only
await master_write.write_register(0x108, 0xFF00, strb=0x2)  # Write byte 1 only

Configuration Space Testing

The register idioms map cleanly onto PCIe-style configuration space -- write-all-ones BAR sizing and friends:

async def test_configuration_space():
    """Test PCIe-style configuration space access."""

    # Test device identification
    device_id = await master_read.read_register(0x000)
    vendor_id = await master_read.read_register(0x002)

    # Test configuration registers
    await master_write.write_register(0x004, 0x00000006)  # Enable bus master
    command = await master_read.read_register(0x004)
    assert (command & 0x6) == 0x6, "Bus master not enabled"

    # Test BAR configuration
    await master_write.write_register(0x010, 0xFFFFFFFF)  # Write all 1s
    bar0_size = await master_read.read_register(0x010)    # Read back
    size = (~bar0_size + 1) & 0xFFFFFFFF
    print(f"BAR0 size: {size} bytes")

Peripheral Control Interface

A typical poll-until-done peripheral sequence:

async def test_peripheral_control():
    """Test typical peripheral control interface."""

    # Configure peripheral
    await master_write.write_register(0x000, 0x00000001)  # Enable peripheral
    await master_write.write_register(0x004, 0x12345678)  # Set data value
    await master_write.write_register(0x008, 0x00000080)  # Start operation

    # Wait for completion
    while True:
        status = await master_read.read_register(0x00C)
        if status & 0x1:  # Done bit
            break
        await Timer(100, units='ns')

    # Read results
    result = await master_read.read_register(0x010)
    error_status = await master_read.read_register(0x014)

    assert error_status == 0, f"Operation failed with error: {error_status}"
    return result

Memory-Mapped FIFO Testing

A memory-mapped FIFO, exercised through its status and data registers:

async def test_memory_mapped_fifo():
    """Test memory-mapped FIFO interface."""

    # Check FIFO status
    status = await master_read.read_register(0x100)  # FIFO status
    empty = (status >> 0) & 1
    full = (status >> 1) & 1
    count = (status >> 8) & 0xFF

    print(f"FIFO: empty={empty}, full={full}, count={count}")

    # Write data to FIFO
    test_data = [0x11111111, 0x22222222, 0x33333333, 0x44444444]
    for data in test_data:
        await master_write.write_register(0x104, data)  # FIFO data register

    # Read data from FIFO
    read_data = []
    for _ in range(len(test_data)):
        data = await master_read.read_register(0x108)  # FIFO read register
        read_data.append(data)

    assert read_data == test_data, "FIFO data mismatch"

Performance Optimization

Single-Beat Transfer Benefits

The single-beat model isn't just less protocol -- it's less testbench. With no bursts, slave state machines stay small, buffering requirements are minimal, and addresses map straight onto registers with no burst decode in between, so a read can complete in a cycle. Multiple transactions may still be outstanding, but with no IDs to reorder against they're matched in simple issue order rather than scoreboarded by ID.

Timing Optimization

Slave response latency is a construction parameter, so you can model a slow register block without touching your stimulus:

# Slave response latency is configured at construction time
slave_read = AXIL4SlaveRead(
    dut, clk, "s_axil_",
    memory_model=memory,
    response_delay=1,   # Cycles before the R response is sent
)

Debug and Analysis

Register Access Logging

Hand the components a logger and every register access is logged at DEBUG level -- address, data, strobe patterns, response codes:

# Pass a logger at construction; all register accesses are logged at
# DEBUG level with address, data, strobe patterns, and response codes.
master_read = AXIL4MasterRead(dut, clk, "m_axil_", log=my_logger)
master_write = AXIL4MasterWrite(dut, clk, "m_axil_", log=my_logger)

If you want your own view of the traffic, callbacks on the underlying channels hand you the packets:

# Attach callbacks to the underlying channels to build custom access reports
accesses = []

master_read.r_channel.add_callback(lambda pkt: accesses.append(('R', pkt)))
master_write.b_channel.add_callback(lambda pkt: accesses.append(('B', pkt)))

Performance Analysis

Worth tracking on a Lite bus:

  • Register Access Frequency: per-register hit counts -- which registers are actually hot
  • Response Latency: read and write response timing
  • Bus Utilization: how much of the time the bus is actually moving data
  • Error Rate: how often SLVERR/DECERR shows up

Configuration Examples

Standard 32-bit Configuration

The setup most designs want. Strobe width follows from data_width:

# Typical 32-bit AXIL4 configuration (strobe width derives from data_width)
axil_config = {
    'data_width': 32,
    'addr_width': 32,
    'timeout_cycles': 1000,
}

master_read = AXIL4MasterRead(dut, clk, "m_axil_", **axil_config)
master_write = AXIL4MasterWrite(dut, clk, "m_axil_", **axil_config)

64-bit AXIL4 Configuration

Lite at 64 bits, for wider register blocks:

# 64-bit wide AXIL4 configuration for high-performance applications
axil_64_config = {
    'data_width': 64,
    'addr_width': 64,     # Extended addressing
}

master_read = AXIL4MasterRead(dut, clk, "m_axil_", **axil_64_config)

Same GAXI machinery underneath -- just a lot less protocol on the wire.