|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
signal_mapping_helper.py¶
Simplified GAXI/FIFO Signal Mapping with pattern matching against top-level ports. This module uses pattern matching against actual DUT ports with parameter combinations to find the correct signal mappings automatically, with support for manual signal mapping override.
Overview¶
The signal_mapping_helper.py module provides automatic signal discovery and mapping for GAXI and FIFO protocols. It can automatically discover signals using pattern matching or accept manual signal mappings. The module handles prefix management for cocotb compatibility and provides comprehensive error reporting.
Key Features¶
- Automatic signal discovery using pattern matching against DUT ports
- Manual signal mapping override with comprehensive validation
- Prefix handling for cocotb Bus compatibility
- Protocol support for GAXI and FIFO (master/slave variants)
- Multi-signal and single-signal modes for different DUT configurations
- Rich error reporting with detailed diagnostics
- Thread-safe operation for parallel testing environments
Constants and Patterns¶
Protocol Modes¶
# Standard FIFO modes (for parameter passing to RTL)
FIFO_VALID_MODES = ['fifo_mux', 'fifo_flop']
# Standard GAXI modes (for parameter passing to RTL)
GAXI_VALID_MODES = ['skid', 'fifo_mux', 'fifo_flop']
Signal Patterns¶
The module includes comprehensive signal patterns for different protocols:
GAXI Patterns¶
- Master-side patterns: For masters and write monitors
- Slave-side patterns: For slaves and read monitors
FIFO Patterns¶
- Write-side patterns: For masters and write monitors
- Read-side patterns: For slaves and read monitors
Protocol Signal Configurations¶
PROTOCOL_SIGNAL_CONFIGS = {
'fifo_master': {
'signal_map': {
'i_write': FIFO_BASE_PATTERNS['write_base'],
'o_wr_full': FIFO_BASE_PATTERNS['full_base']
},
'optional_signal_map': {
'multi_sig_false': FIFO_BASE_PATTERNS['wr_data_base'],
'multi_sig_true': FIFO_BASE_PATTERNS['wr_field_base']
}
},
# ... other protocol configurations
}
Core Class¶
SignalResolver¶
Signal resolver using pattern matching against actual top-level DUT ports with support for manual signal mapping.
Constructor¶
SignalResolver(protocol_type: str, dut, bus, log, component_name: str,
prefix='', field_config=None, multi_sig: bool = False,
bus_name: str = '', pkt_prefix: str = '', mode: str = None,
super_debug: bool = False, signal_map: Optional[Dict[str, str]] = None)
Parameters:
- protocol_type: Protocol type ('fifo_master', 'fifo_slave', 'gaxi_master', 'gaxi_slave')
- dut: Device under test
- bus: Bus object from BusDriver/BusMonitor (can be None initially)
- log: Logger instance (can be None)
- component_name: Component name for error messages
- prefix: Prefix that cocotb will prepend to signal names
- field_config: Field configuration (required for multi_sig=True)
- multi_sig: Whether using multi-signal mode
- bus_name: Bus/channel name
- pkt_prefix: Packet field prefix
- mode: Protocol mode (kept for RTL parameter)
- super_debug: Enable detailed signal resolution debugging
- signal_map: Optional manual signal mapping (bypasses automatic discovery)
Signal Map Format¶
When using manual signal_map, the keys vary by protocol:
GAXI Protocols:
- 'valid': Valid signal name
- 'ready': Ready signal name
- 'data': Data signal name (single-signal mode)
- Field names: Individual field signal names (multi-signal mode)
FIFO Master:
- 'write': Write signal name
- 'full': Full signal name
- 'data': Data signal name (single-signal mode)
- Field names: Individual field signal names (multi-signal mode)
FIFO Slave:
- 'read': Read signal name
- 'empty': Empty signal name
- 'data': Data signal name (single-signal mode)
- Field names: Individual field signal names (multi-signal mode)
# Automatic signal discovery
resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=bus,
log=log,
component_name='TestMaster',
prefix='test_',
multi_sig=False
)
# Manual signal mapping
signal_map = {
'valid': 'master_valid',
'ready': 'slave_ready',
'data': 'transfer_data'
}
resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=bus,
log=log,
component_name='TestMaster',
signal_map=signal_map
)
Methods¶
apply_to_component(component)¶
Apply resolved signals to component as attributes with comprehensive validation.
Parameters:
- component: Component to apply signals to
Raises:
- RuntimeError: If signal linkage fails with detailed diagnostic information
# Apply signals to component
try:
resolver.apply_to_component(self)
log.info("Signal mapping successful")
except RuntimeError as e:
log.error(f"Signal mapping failed: {e}")
raise
get_signal(logical_name: str)¶
Get a resolved signal by logical name.
Parameters:
- logical_name: Logical signal name from SignalResolver
Returns: Signal object or None
# Get specific signals
valid_signal = resolver.get_signal('o_valid')
data_signal = resolver.get_signal('data_sig')
has_signal(logical_name: str) -> bool¶
Check if a signal was found and is not None.
get_signal_lists()¶
Get the _signals and _optional_signals lists for cocotb Bus initialization.
Returns: Tuple of (_signals, _optional_signals)
get_stats() -> Dict[str, Any]¶
Get statistics about signal resolution.
Returns: Dictionary with comprehensive resolution statistics
stats = resolver.get_stats()
print(f"Resolution rate: {stats['resolution_rate']:.1f}%")
print(f"Signal mapping source: {stats['signal_mapping_source']}")
print(f"Protocol: {stats['protocol_type']}")
Usage Patterns¶
Automatic Signal Discovery¶
class GAXIMaster:
def __init__(self, dut, field_config, log):
self.dut = dut
self.log = log
# Create bus for signal access
self.bus = GAXIBus(dut, "test_", log)
# Automatic signal discovery
self.resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=self.bus,
log=log,
component_name='GAXIMaster',
prefix='test_', # Prefix cocotb will add
field_config=field_config,
multi_sig=True, # Use individual field signals
super_debug=False # Enable for debugging
)
# Apply signals to component
self.resolver.apply_to_component(self)
# Now we can access signals as attributes
# self.valid_sig, self.ready_sig, self.field_addr_sig, etc.
@cocotb.coroutine
def send_transaction(self, packet):
"""Send transaction using resolved signals"""
# Drive valid
self.valid_sig.value = 1
# Drive field signals
if hasattr(self, 'field_addr_sig'):
self.field_addr_sig.value = packet.addr
if hasattr(self, 'field_data_sig'):
self.field_data_sig.value = packet.data
# Wait for ready
while self.ready_sig.value != 1:
yield RisingEdge(self.dut.clk)
yield RisingEdge(self.dut.clk)
# Deassert valid
self.valid_sig.value = 0
Manual Signal Mapping¶
class FIFOMaster:
def __init__(self, dut, field_config, log):
self.dut = dut
self.log = log
# Create bus
self.bus = FIFOBus(dut, "fifo_", log)
# Manual signal mapping (when automatic discovery fails)
signal_map = {
'write': 'wr_en', # Write enable signal
'full': 'fifo_full', # FIFO full signal
'data': 'wr_data' # Write data signal
}
self.resolver = SignalResolver(
protocol_type='fifo_master',
dut=dut,
bus=self.bus,
log=log,
component_name='FIFOMaster',
signal_map=signal_map
)
# Apply signals
self.resolver.apply_to_component(self)
# Access signals: self.write_sig, self.full_sig, self.data_sig
@cocotb.coroutine
def write_data(self, data):
"""Write data to FIFO"""
# Check if FIFO is full
if self.full_sig.value == 1:
self.log.warning("FIFO is full, cannot write")
return False
# Write data
self.data_sig.value = data
self.write_sig.value = 1
yield RisingEdge(self.dut.clk)
self.write_sig.value = 0
return True
Multi-Signal Mode¶
class MultiSignalGAXISlave:
def __init__(self, dut, field_config, log):
# Field configuration with multiple fields
self.field_config = field_config # Contains addr, data, cmd fields
# Multi-signal mode - each field has its own signal
self.resolver = SignalResolver(
protocol_type='gaxi_slave',
dut=dut,
bus=None, # Will be set later
log=log,
component_name='MultiSignalSlave',
field_config=field_config,
multi_sig=True, # Enable multi-signal mode
prefix='slave_'
)
# Create bus after resolver is configured
signals, optional_signals = self.resolver.get_signal_lists()
self.bus = GAXIBus(dut, "slave_", log, signals, optional_signals)
self.resolver.bus = self.bus
# Apply signals
self.resolver.apply_to_component(self)
# Now we have: self.valid_sig, self.ready_sig,
# self.field_addr_sig, self.field_data_sig, self.field_cmd_sig
@cocotb.coroutine
def monitor_transactions(self):
"""Monitor incoming transactions"""
while True:
yield RisingEdge(self.dut.clk)
if self.valid_sig.value == 1 and self.ready_sig.value == 1:
# Capture transaction
transaction = {}
transaction['addr'] = int(self.field_addr_sig.value)
transaction['data'] = int(self.field_data_sig.value)
transaction['cmd'] = int(self.field_cmd_sig.value)
self.process_transaction(transaction)
Error Handling and Debugging¶
class DebugSignalResolver:
def __init__(self, dut, log):
self.dut = dut
self.log = log
try:
# Attempt automatic discovery with debug enabled
self.resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=None,
log=log,
component_name='DebugMaster',
super_debug=True, # Enable detailed debugging
prefix='debug_'
)
# Check resolution statistics
stats = self.resolver.get_stats()
self.log.info(f"Signal resolution: {stats}")
if stats['missing_required'] > 0:
self.log.error("Missing required signals, trying manual mapping")
self._try_manual_mapping()
except ValueError as e:
self.log.error(f"Signal resolution failed: {e}")
self._try_manual_mapping()
def _try_manual_mapping(self):
"""Try manual signal mapping as fallback"""
# Inspect available signals
available_signals = self._get_available_signals()
self.log.info(f"Available signals: {available_signals}")
# Create manual mapping based on available signals
signal_map = self._create_manual_mapping(available_signals)
if signal_map:
self.resolver = SignalResolver(
protocol_type='gaxi_master',
dut=self.dut,
bus=None,
log=self.log,
component_name='DebugMaster',
signal_map=signal_map
)
else:
raise RuntimeError("Unable to create signal mapping")
def _get_available_signals(self):
"""Get list of available signals on DUT"""
signals = []
for attr_name in dir(self.dut):
if not attr_name.startswith('_'):
try:
attr = getattr(self.dut, attr_name)
if hasattr(attr, 'value'):
signals.append(attr_name)
except:
pass
return signals
def _create_manual_mapping(self, available_signals):
"""Create manual mapping from available signals"""
signal_map = {}
# Look for common signal patterns
for signal in available_signals:
if 'valid' in signal.lower():
signal_map['valid'] = signal
elif 'ready' in signal.lower():
signal_map['ready'] = signal
elif 'data' in signal.lower():
signal_map['data'] = signal
# Return mapping if we found required signals
if 'valid' in signal_map and 'ready' in signal_map:
return signal_map
else:
return None
Advanced Configuration¶
class AdvancedSignalMapping:
def __init__(self, dut, config):
self.dut = dut
self.config = config
self.resolvers = {}
# Create multiple resolvers for different interfaces
self._setup_multiple_interfaces()
def _setup_multiple_interfaces(self):
"""Set up multiple protocol interfaces"""
# Master interface
master_map = {
'valid': 'master_valid',
'ready': 'master_ready',
'data': 'master_data'
}
self.resolvers['master'] = SignalResolver(
protocol_type='gaxi_master',
dut=self.dut,
bus=None,
log=self.log,
component_name='AdvancedMaster',
signal_map=master_map,
prefix='m_'
)
# Slave interface
slave_map = {
'valid': 'slave_valid',
'ready': 'slave_ready',
'data': 'slave_data'
}
self.resolvers['slave'] = SignalResolver(
protocol_type='gaxi_slave',
dut=self.dut,
bus=None,
log=self.log,
component_name='AdvancedSlave',
signal_map=slave_map,
prefix='s_'
)
# Apply all resolvers
for name, resolver in self.resolvers.items():
resolver.apply_to_component(self)
self.log.info(f"Applied {name} interface signals")
def get_interface_stats(self):
"""Get statistics for all interfaces"""
stats = {}
for name, resolver in self.resolvers.items():
stats[name] = resolver.get_stats()
return stats
Protocol-Specific Usage¶
class ProtocolSpecificExample:
"""Examples for different protocol configurations"""
def setup_gaxi_write_channel(self, dut, log):
"""Set up GAXI write channel signals"""
# Write address channel
aw_resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=None,
log=log,
component_name='AW_Channel',
signal_map={
'valid': 'awvalid',
'ready': 'awready',
'awid': 'awid',
'awaddr': 'awaddr',
'awlen': 'awlen'
},
multi_sig=True
)
return aw_resolver
def setup_fifo_interface(self, dut, log):
"""Set up FIFO interface signals"""
# FIFO write interface
fifo_resolver = SignalResolver(
protocol_type='fifo_master',
dut=dut,
bus=None,
log=log,
component_name='FIFO_Write',
signal_map={
'write': 'wr_en',
'full': 'full',
'data': 'din'
}
)
return fifo_resolver
def setup_custom_protocol(self, dut, log):
"""Set up custom protocol using manual mapping"""
# Custom protocol with specific signal names
custom_map = {
'valid': 'req_valid',
'ready': 'req_ready',
'cmd': 'command',
'addr': 'address',
'data': 'payload'
}
custom_resolver = SignalResolver(
protocol_type='gaxi_master', # Use closest matching protocol
dut=dut,
bus=None,
log=log,
component_name='CustomProtocol',
signal_map=custom_map,
multi_sig=True
)
return custom_resolver
Test Framework Integration¶
@cocotb.test()
def signal_mapping_test(dut):
"""Test with automatic signal mapping"""
# Create field configuration
field_config = FieldConfig()
field_config.add_field(FieldDefinition("addr", 32))
field_config.add_field(FieldDefinition("data", 32))
# Set up signal resolver
resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=None,
log=cocotb.log,
component_name='TestMaster',
field_config=field_config,
multi_sig=True,
super_debug=True
)
# Create and configure bus
signals, optional_signals = resolver.get_signal_lists()
bus = GAXIBus(dut, "", cocotb.log, signals, optional_signals)
resolver.bus = bus
# Apply to test master
master = TestMaster(dut, resolver)
# Verify signal mapping
assert hasattr(master, 'valid_sig'), "Valid signal not mapped"
assert hasattr(master, 'ready_sig'), "Ready signal not mapped"
# Run test with mapped signals
yield master.run_test_sequence()
# Check mapping statistics
stats = resolver.get_stats()
cocotb.log.info(f"Signal mapping stats: {stats}")
assert stats['resolved_signals'] >= 2, "Insufficient signals resolved"
assert stats['conflicts'] == 0, "Signal conflicts detected"
class TestMaster:
def __init__(self, dut, resolver):
self.dut = dut
resolver.apply_to_component(self)
@cocotb.coroutine
def run_test_sequence(self):
"""Run test using mapped signals"""
for i in range(10):
# Use resolved signals
self.valid_sig.value = 1
self.field_addr_sig.value = 0x1000 + i*4
self.field_data_sig.value = i * 0x100
yield RisingEdge(self.dut.clk)
self.valid_sig.value = 0
Error Handling and Diagnostics¶
Comprehensive Error Reporting¶
The SignalResolver provides detailed error reporting when signal mapping fails:
try:
resolver.apply_to_component(component)
except RuntimeError as e:
# Error message includes:
# - Failed signal details (DUT signal name, cocotb signal name, target attribute)
# - Signal type (REQUIRED vs DATA/OPTIONAL)
# - Successful linkages (for comparison)
# - Bus diagnostic information
# - Prefix handling details
# - Signal lists passed to Bus
# - Manual signal map info (if used)
log.error(f"Detailed signal mapping failure: {e}")
Debugging Support¶
# Enable super debug for detailed tracing
resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=bus,
log=log,
component_name='DebugComponent',
super_debug=True # Enables detailed logging
)
# Check resolution statistics
stats = resolver.get_stats()
print(f"Total ports found: {stats['total_ports_found']}")
print(f"Parameter combinations: {stats['parameter_combinations']}")
print(f"Resolution rate: {stats['resolution_rate']:.1f}%")
# Dump log messages if logger not available
if not log:
resolver.dump_log_messages()
Best Practices¶
1. Start with Automatic Discovery¶
# Try automatic discovery first
try:
resolver = SignalResolver('gaxi_master', dut, bus, log, 'Component')
resolver.apply_to_component(self)
except RuntimeError:
# Fall back to manual mapping
signal_map = create_manual_mapping()
resolver = SignalResolver('gaxi_master', dut, bus, log, 'Component', signal_map=signal_map)
2. Use Manual Mapping for Non-Standard Signals¶
# For custom or non-standard signal names
signal_map = {
'valid': 'my_custom_valid',
'ready': 'my_custom_ready',
'data': 'my_custom_data'
}
resolver = SignalResolver(protocol_type, dut, bus, log, name, signal_map=signal_map)
3. Handle Prefix Correctly¶
# Prefix should match what cocotb Bus will add
resolver = SignalResolver(
protocol_type='gaxi_master',
dut=dut,
bus=bus,
log=log,
component_name='Master',
prefix='master_' # This should match Bus prefix
)
4. Validate Signal Mapping¶
# Always check mapping was successful
resolver.apply_to_component(self)
# Verify expected signals exist
required_signals = ['valid_sig', 'ready_sig', 'data_sig']
for signal_name in required_signals:
assert hasattr(self, signal_name), f"Missing signal: {signal_name}"
5. Use Statistics for Debugging¶
# Check resolution statistics for debugging
stats = resolver.get_stats()
if stats['resolution_rate'] < 100:
log.warning(f"Incomplete signal resolution: {stats}")
if stats['conflicts'] > 0:
log.error(f"Signal conflicts detected: {stats['conflict_details']}")
The SignalResolver provides a robust foundation for signal mapping across different protocols, with comprehensive error handling, flexible configuration options, and extensive debugging support for verification environments.