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

gaxi_factories.py

Factory functions for creating GAXI components without the constructor boilerplate. They pass your parameters straight through — what they add is sensible defaults (empty prefixes work for most DUTs), a standard field configuration when you don't supply one, and one-call creation of a whole master/slave/monitor/scoreboard environment.

Overview

Every factory here builds the components documented on their own pages, with the same parameters, in the same order, meaning the same things. Old testbenches keep working: legacy parameter names are still accepted, even the ones that are now ignored (they say so). The factories also create a shared MemoryModel for you when you don't supply one, which is what you want more often than not.

Key Features

  • Sensible defaults — empty string prefixes cover most DUTs
  • Same parameter handling in every factory
  • Backward compatible — every old parameter still accepted, a few of them ignored and documented as such
  • Memory model auto-created when you don't pass one
  • Whole-system factories that return every component in a dict, ready to wire up

Field Configuration

get_default_field_config(data_width=32)

The standard GAXI field config: one data field of the given width. Most smoke tests never need more.

Parameters: - data_width: Data width in bits (default: 32)

Returns: FieldConfig object for standard data field

# Get default 32-bit data configuration
config = get_default_field_config()

# Get 64-bit data configuration
config_64 = get_default_field_config(data_width=64)

Component Factories

create_gaxi_master()

Build a GAXIMaster with defaults filled in.

create_gaxi_master(dut, title, prefix, clock, field_config=None, packet_class=None,
                   randomizer=None, memory_model=None, memory_fields=None, log=None,
                   signal_map=None, optional_signal_map=None, field_mode=False,
                   multi_sig=False, mode='skid', bus_name='', pkt_prefix='',
                   **kwargs)

Parameters: - dut: Device under test - title: Component title - prefix: Signal prefix - clock: Clock signal - field_config: Field configuration (default: standard data field) - packet_class: Packet class produced by the component's pipeline (None = GAXIPacket). Wired through the _build_packet() hook - randomizer: Timing randomizer (default: standard master constraints) - memory_model: Memory model for transactions (optional) - memory_fields: Field mapping for memory operations (unused - kept for compatibility) - log: Logger instance (default: dut's logger) - signal_map: Manual signal mapping dict (forwarded to the component; None = automatic signal discovery) - optional_signal_map: Optional signal mapping (unused - kept for compatibility) - field_mode: Field mode (unused - kept for compatibility) - multi_sig: Whether using multi-signal mode - mode: Operating mode ('skid', 'fifo_mux', 'fifo_flop') - bus_name: Bus/channel name - pkt_prefix: Packet field prefix - **kwargs: Additional arguments

Returns: GAXIMaster instance

# Basic master creation
master = create_gaxi_master(
    dut=dut,
    title="TestMaster",
    prefix="master_",
    clock=dut.clk
)

# Advanced master with custom configuration
master = create_gaxi_master(
    dut=dut,
    title="AdvancedMaster",
    prefix="",
    clock=dut.clk,
    field_config=custom_field_config,
    memory_model=shared_memory,
    multi_sig=True,
    mode='fifo_flop',
    log=custom_logger
)

create_gaxi_slave()

Build a GAXISlave with defaults filled in.

create_gaxi_slave(dut, title, prefix, clock, field_config=None, field_mode=False,
                  packet_class=None, randomizer=None, memory_model=None,
                  memory_fields=None, log=None, mode='skid', signal_map=None,
                  optional_signal_map=None, multi_sig=False, bus_name='',
                  pkt_prefix='', **kwargs)

Parameters: Same as create_gaxi_master() with slave-specific defaults

Returns: GAXISlave instance

# Basic slave creation
slave = create_gaxi_slave(
    dut=dut,
    title="TestSlave",
    prefix="slave_",
    clock=dut.clk
)

# Slave with shared memory model
slave = create_gaxi_slave(
    dut=dut,
    title="MemorySlave",
    prefix="",
    clock=dut.clk,
    memory_model=shared_memory,
    mode='skid'
)

create_gaxi_monitor()

Build a GAXIMonitor watching either side of the interface.

create_gaxi_monitor(dut, title, prefix, clock, field_config=None, is_slave=False,
                    log=None, mode='skid', bus_name='', pkt_prefix='',
                    multi_sig=False, signal_map=None, packet_class=None, **kwargs)

Parameters: - dut: Device under test - title: Component title - prefix: Signal prefix - clock: Clock signal - field_config: Field configuration (default: standard data field) - is_slave: If True, monitor slave side; if False, monitor master side - log: Logger instance (default: dut's logger) - mode: Operating mode ('skid', 'fifo_mux', 'fifo_flop') - multi_sig: Whether using multi-signal mode - bus_name: Bus/channel name - pkt_prefix: Packet field prefix - signal_map: Manual signal mapping dict (forwarded to the component; None = automatic signal discovery) - packet_class: Packet class produced by the receive pipeline (None = GAXIPacket). Wired through the _build_packet() hook - **kwargs: Additional arguments

Returns: GAXIMonitor instance

# Master-side monitor
master_monitor = create_gaxi_monitor(
    dut=dut,
    title="MasterMonitor",
    prefix="",
    clock=dut.clk,
    is_slave=False
)

# Slave-side monitor
slave_monitor = create_gaxi_monitor(
    dut=dut,
    title="SlaveMonitor",
    prefix="",
    clock=dut.clk,
    is_slave=True,
    mode='fifo_flop'
)

Custom packet classes

Every component factory forwards packet_class into the component's _build_packet() hook, so the whole pipeline — receive path, create_packet(), and the master transmit path — produces your class instead of GAXIPacket:

class MyPacket(GAXIPacket):
    pass

slave = create_gaxi_slave(dut, "Slave", "", clock, packet_class=MyPacket)
monitor = create_gaxi_monitor(dut, "Mon", "", clock, packet_class=MyPacket)

# Or apply it to a whole component set at once
components = create_gaxi_components(dut, clock, packet_class=MyPacket)

pkt = monitor._recvQ.popleft()
assert isinstance(pkt, MyPacket)   # protocol subclass survives the pipeline

packet_class covers classes constructible as MyPacket(field_config). If your packet needs additional constructor arguments, subclass the component and override _build_packet() instead — see the component base docs.

Leave packet_class out and nothing changes: components produce plain GAXIPacket instances, as they always have.

create_gaxi_scoreboard()

Build a GAXIScoreboard.

create_gaxi_scoreboard(name, field_config=None, log=None)

Parameters: - name: Scoreboard name - field_config: Field configuration (default: standard data field) - log: Logger instance

Returns: GAXIScoreboard instance

# Basic scoreboard
scoreboard = create_gaxi_scoreboard("TestScoreboard")

# Scoreboard with custom configuration
scoreboard = create_gaxi_scoreboard(
    name="CustomScoreboard",
    field_config=custom_field_config,
    log=custom_logger
)

System Creation Factories

create_gaxi_components()

Build the whole bench in one call: master, slave, a monitor for each side, a scoreboard, and a shared memory model.

create_gaxi_components(dut, clock, title_prefix="", field_config=None,
                       field_mode=False, packet_class=None, memory_model=None,
                       log=None, mode='skid', signal_map=None, optional_signal_map=None,
                       multi_sig=False, bus_name='', pkt_prefix='',
                       **kwargs)

Parameters: - dut: Device under test - clock: Clock signal - title_prefix: Prefix for component titles - field_config: Field configuration (default: standard data field) - field_mode: Field mode (unused - kept for compatibility) - packet_class: Packet class produced by the component's pipeline (None = GAXIPacket). Wired through the _build_packet() hook - memory_model: Memory model for components (auto-created if None) - log: Logger instance - mode: Operating mode for slave/monitor - signal_map: Manual signal mapping dict (forwarded to the component; None = automatic signal discovery) - optional_signal_map: Optional signal mapping (unused - kept for compatibility) - multi_sig: Whether using multi-signal mode - bus_name: Bus/channel name - pkt_prefix: Packet field prefix - **kwargs: Additional configuration passed to all components

Returns: Dictionary containing all created components

# Create complete GAXI system
components = create_gaxi_components(
    dut=dut,
    clock=dut.clk,
    title_prefix="GAXI_"
)

# Access components
master = components['master']
slave = components['slave']
master_monitor = components['master_monitor']
slave_monitor = components['slave_monitor']
scoreboard = components['scoreboard']
memory_model = components['memory_model']

create_gaxi_system()

Alias for create_gaxi_components() with a trimmed parameter list.

create_gaxi_system(dut, clock, title_prefix="", field_config=None,
                   memory_model=None, log=None, bus_name='', pkt_prefix='',
                   **kwargs)

Parameters: Simplified version of create_gaxi_components() parameters

Returns: Dictionary containing all created components and shared resources

# Create GAXI system with clean API
system = create_gaxi_system(
    dut=dut,
    clock=dut.clk,
    title_prefix="System_",
    field_config=custom_config
)

create_gaxi_test_environment()

The everything-included option: all components plus a few convenience functions (send_data, read_memory, write_memory, get_all_stats) so a simple test never touches the components directly.

create_gaxi_test_environment(dut, clock, bus_name='', pkt_prefix='', **kwargs)

Parameters: - dut: Device under test - clock: Clock signal - bus_name: Bus/channel name - pkt_prefix: Packet field prefix - **kwargs: Configuration overrides including: - title_prefix: Component title prefix (default: 'GAXI_') - field_config: Field configuration - data_width: Data width (default: 32) - memory_size: Memory size in lines (default: 1024) - log: Logger instance

Returns: Dictionary with complete test environment including convenience functions

# Create ready-to-use test environment
env = create_gaxi_test_environment(
    dut=dut,
    clock=dut.clk,
    data_width=64,
    memory_size=2048
)

# Use convenience functions
await env['send_data'](0xDEADBEEF)
data = env['read_memory'](0x1000)
env['write_memory'](0x1000, 0x12345678)
all_stats = env['get_all_stats']()

Usage Patterns

Basic Component Creation

@cocotb.test()
async def test_basic_gaxi(dut):
    """Basic GAXI test with factory-created components"""

    # Create individual components
    master = create_gaxi_master(
        dut=dut,
        title="TestMaster",
        prefix="",
        clock=dut.clk
    )

    slave = create_gaxi_slave(
        dut=dut,
        title="TestSlave", 
        prefix="",
        clock=dut.clk
    )

    # Test basic functionality
    await master.reset_bus()
    await slave.reset_bus()

    # Send test transaction
    packet = master.create_packet(data=0xDEADBEEF)
    await master.send(packet)

    # Verify reception
    await Timer(100, units='ns')
    received = slave.get_observed_packets()
    assert len(received) > 0

Complete System Creation

@cocotb.test()
async def test_complete_system(dut):
    """Test with complete GAXI system"""

    # Create complete system
    # (data_width/memory_size are create_gaxi_test_environment options;
    #  create_gaxi_system takes a field_config and memory_model instead)
    system = create_gaxi_system(
        dut=dut,
        clock=dut.clk,
        title_prefix="Test_",
        log=log
    )

    # Extract components
    master = system['master']
    slave = system['slave']
    master_monitor = system['master_monitor']
    slave_monitor = system['slave_monitor']
    scoreboard = system['scoreboard']
    memory = system['memory_model']

    # Connect monitors to scoreboard
    master_monitor.add_callback(scoreboard.add_expected)
    slave_monitor.add_callback(scoreboard.add_actual)

    # Run test sequence
    await run_test_sequence(master, slave)

    # Check results
    stats = {
        'master': master.get_stats(),
        'slave': slave.get_stats(),
        'master_monitor': master_monitor.get_stats(),
        'slave_monitor': slave_monitor.get_stats(),
        'memory': memory.get_stats()
    }

    log.info(f"Test completed: {stats}")

Test Environment Creation

@cocotb.test()
async def test_with_environment(dut):
    """Test using complete test environment"""

    # Create test environment with convenience functions
    env = create_gaxi_test_environment(
        dut=dut,
        clock=dut.clk,
        title_prefix="ENV_",
        data_width=64,
        memory_size=2048
    )

    # Use convenience functions for testing
    test_data = [0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x87654321]

    for i, data in enumerate(test_data):
        # Send data using convenience function
        await env['send_data'](data)

        # Write to memory using convenience function
        env['write_memory'](0x1000 + i*4, data)

    # Read back and verify
    for i, expected_data in enumerate(test_data):
        addr = 0x1000 + i*4
        read_data = env['read_memory'](addr)

        # Convert bytearray to integer for comparison
        if isinstance(read_data, bytearray):
            read_value = int.from_bytes(read_data, byteorder='little')
        else:
            read_value = read_data

        assert read_value == expected_data, f"Memory mismatch at 0x{addr:X}"

    # Get comprehensive statistics
    all_stats = env['get_all_stats']()
    log.info(f"Environment test completed: {all_stats}")

Advanced Configuration

async def create_advanced_gaxi_system(dut, clock):
    """Create advanced GAXI system with custom configuration"""

    # Create custom field configuration
    field_config = FieldConfig()
    field_config.add_field(FieldDefinition("addr", 32, format="hex"))
    field_config.add_field(FieldDefinition("data", 64, format="hex"))
    field_config.add_field(FieldDefinition("cmd", 4, format="hex", encoding={
        0x0: "NOP", 0x1: "READ", 0x2: "WRITE", 0x3: "BURST"
    }))

    # Create shared memory model
    memory = MemoryModel(
        num_lines=4096,
        bytes_per_line=8,  # 64-bit data
        log=dut._log
    )

    # Create components with advanced configuration
    master = create_gaxi_master(
        dut=dut,
        title="AdvancedMaster",
        prefix="m_",
        clock=clock,
        field_config=field_config,
        memory_model=memory,
        multi_sig=True,
        mode='fifo_flop',
    )

    slave = create_gaxi_slave(
        dut=dut,
        title="AdvancedSlave",
        prefix="s_",
        clock=clock,
        field_config=field_config,
        memory_model=memory,
        multi_sig=True,
        mode='fifo_flop',
    )

    # Create monitors for both sides
    master_monitor = create_gaxi_monitor(
        dut=dut,
        title="MasterSideMonitor",
        prefix="m_",
        clock=clock,
        field_config=field_config,
        is_slave=False,
        multi_sig=True
    )

    slave_monitor = create_gaxi_monitor(
        dut=dut,
        title="SlaveSideMonitor",
        prefix="s_",
        clock=clock,
        field_config=field_config,
        is_slave=True,
        multi_sig=True,
        mode='fifo_flop'
    )

    # Create scoreboard
    scoreboard = create_gaxi_scoreboard(
        name="AdvancedScoreboard",
        field_config=field_config,
        log=dut._log
    )

    return {
        'master': master,
        'slave': slave,
        'master_monitor': master_monitor,
        'slave_monitor': slave_monitor,
        'scoreboard': scoreboard,
        'memory_model': memory,
        'field_config': field_config
    }

Multi-Instance Systems

Several masters, one slave, one shared memory — the factories make the wiring tedious rather than hard, so it helps to wrap it in a function you'll reuse.

async def create_multi_master_system(dut, clock):
    """Create system with multiple masters and one slave"""

    # Shared resources
    shared_memory = MemoryModel(num_lines=2048, bytes_per_line=4)
    shared_config = get_default_field_config(data_width=32)

    # Create multiple masters
    masters = {}
    for i in range(4):
        masters[f'master_{i}'] = create_gaxi_master(
            dut=dut,
            title=f"Master{i}",
            prefix=f"m{i}_",
            clock=clock,
            field_config=shared_config,
            memory_model=shared_memory
        )

    # Create single slave
    slave = create_gaxi_slave(
        dut=dut,
        title="SharedSlave",
        prefix="s_",
        clock=clock,
        field_config=shared_config,
        memory_model=shared_memory
    )

    # Create monitors for each master
    monitors = {}
    for i in range(4):
        monitors[f'master_{i}_monitor'] = create_gaxi_monitor(
            dut=dut,
            title=f"Master{i}Monitor",
            prefix=f"m{i}_",
            clock=clock,
            field_config=shared_config,
            is_slave=False
        )

    # Slave monitor
    monitors['slave_monitor'] = create_gaxi_monitor(
        dut=dut,
        title="SlaveMonitor",
        prefix="s_",
        clock=clock,
        field_config=shared_config,
        is_slave=True
    )

    # Scoreboard for transaction checking
    scoreboard = create_gaxi_scoreboard(
        name="MultiMasterScoreboard",
        field_config=shared_config
    )

    return {
        'masters': masters,
        'slave': slave,
        'monitors': monitors,
        'scoreboard': scoreboard,
        'shared_memory': shared_memory
    }

Factory Pattern Integration

If your project spins up the same few configurations in every test, put a factory class in front of the factories. Boring, but it pays for itself by the third testbench.

class GAXITestFactory:
    """Factory class for creating GAXI test environments"""

    def __init__(self, dut, clock):
        self.dut = dut
        self.clock = clock
        self.default_config = get_default_field_config()

    def create_basic_system(self, prefix=""):
        """Create basic GAXI system"""
        return create_gaxi_system(
            dut=self.dut,
            clock=self.clock,
            title_prefix=prefix
        )

    def create_performance_test_system(self, data_width=32, memory_size=4096):
        """Create system optimized for performance testing"""
        return create_gaxi_test_environment(
            dut=self.dut,
            clock=self.clock,
            title_prefix="PERF_",
            data_width=data_width,
            memory_size=memory_size
        )

    def create_protocol_test_system(self, multi_sig=True, mode='skid'):
        """Create system for protocol compliance testing"""
        field_config = FieldConfig()
        field_config.add_field(FieldDefinition("addr", 32))
        field_config.add_field(FieldDefinition("data", 32))
        field_config.add_field(FieldDefinition("prot", 3))
        field_config.add_field(FieldDefinition("user", 4))

        return create_gaxi_components(
            dut=self.dut,
            clock=self.clock,
            title_prefix="PROT_",
            field_config=field_config,
            multi_sig=multi_sig,
            mode=mode
        )

    def create_stress_test_system(self):
        """Create system for stress testing"""
        return create_gaxi_test_environment(
            dut=self.dut,
            clock=self.clock,
            title_prefix="STRESS_",
            data_width=64,
            memory_size=8192
        )

# Usage
@cocotb.test()
async def test_with_factory(dut):
    factory = GAXITestFactory(dut, dut.clk)

    # Create different systems for different test phases
    basic_system = factory.create_basic_system("BASIC_")
    perf_system = factory.create_performance_test_system(64, 2048)
    stress_system = factory.create_stress_test_system()

    # Run tests with different systems...

Error Handling and Validation

Parameter Validation

The factories don't hide component validation from you — if a parameter is wrong, the component raises at construction, same as if you'd built it by hand.

# Factories automatically handle parameter validation
try:
    master = create_gaxi_master(
        dut=dut,
        title="TestMaster",
        prefix="",
        clock=dut.clk,
        field_config=invalid_config  # Will be validated and corrected
    )
except Exception as e:
    log.error(f"Factory creation failed: {e}")

Default Fallbacks

Anything you don't pass gets the boring, usually-right default: standard field config, the DUT's logger, empty prefixes.

# Factories provide sensible defaults for all parameters
master = create_gaxi_master(dut, "Master", "", dut.clk)
# Uses: default field config, dut's logger, empty prefixes, etc.

Memory Model Auto-Creation

# Memory model is automatically created if not provided
system = create_gaxi_components(dut, dut.clk)
# Automatically creates MemoryModel(1024, 4) with proper configuration

Best Practices

1. Use Empty String Prefixes for Most Cases

# Empty prefixes work for most DUT configurations

2. Create Complete Systems for Full Testing

# Use create_gaxi_test_environment for comprehensive testing
env = create_gaxi_test_environment(dut, dut.clk)
# Includes all components plus convenience functions

3. Share Memory Models Across Components

One memory, many components — otherwise your master and slave disagree about what's stored where.

# Share memory for consistent state
memory = MemoryModel(1024, 4, log=log)
master = create_gaxi_master(dut, "Master", "", dut.clk, memory_model=memory)
slave = create_gaxi_slave(dut, "Slave", "", dut.clk, memory_model=memory)

4. Use Multi-Signal Mode for Complex Protocols

# Enable multi-signal mode for protocols with many fields
components = create_gaxi_components(dut, dut.clk, multi_sig=True)

5. Use Factory Classes for Complex Testbenches

# Create factory class for standardized test setups
factory = GAXITestFactory(dut, dut.clk)
system = factory.create_protocol_test_system()

Backward Compatibility

The factories keep full backward compatibility:

  • Every parameter that used to exist still exists, in the same position, meaning the same thing
  • Defaults improved only where old code didn't depend on them
  • New options default to the old behavior
  • Legacy names are still accepted (field_mode, optional_signal_map) even when they're ignored; signal_map is forwarded to the component
# Legacy code continues to work unchanged
components = create_gaxi_components(
    dut, clock, title_prefix="Legacy_", field_mode=True,
    signal_map=old_signal_map, optional_signal_map=old_optional_map
)

Use the factories unless you have a reason not to. Everything they do is documented on the component pages, and you can always do it by hand — but for the common case of "give me a working GAXI environment", one call beats five.