|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
signal_mapping_helper.py¶
Automatic signal discovery and mapping for GAXI and FIFO protocols — pattern-matches your DUT's actual port names against known conventions, and accepts a manual map when the RTL insists on being creative.
Overview¶
The least glamorous part of any BFM is figuring out which DUT port is which logical signal. Naming conventions drift (awvalid vs aw_valid vs i_aw_valid), prefixes multiply, and hardcoding names means every new DUT is a scavenger hunt. SignalResolver automates the boring part: it enumerates the DUT's top-level ports, matches them against per-protocol patterns, and applies the results to your component as attributes. When automatic discovery can't cope — and there will be a design where it can't — you pass a signal_map and it uses exactly what you gave it, no guessing. Prefix handling for cocotb Bus compatibility is built in, and failures come with diagnostics instead of a bare exception.
Key Features¶
- Automatic signal discovery by pattern-matching against DUT ports
- Manual signal mapping override when the RTL doesn't follow conventions
- 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
- Detailed error reporting when resolution fails
- 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 carries pattern sets for each supported protocol and side:
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¶
The resolver itself: pattern-match against the DUT's top-level ports, or use a manual map verbatim.
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)
Multi-signal mode is for DUTs that break the bus into one port per field instead of a single wide data signal. Pass multi_sig=True and a FieldConfig, and each field resolves to its own attribute (field_addr_sig, field_data_sig, ...). Single-signal mode gives you one data_sig and the packing is the framework's problem.
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¶
When apply_to_component() fails, the exception tells you why — which is the difference between a five-minute fix and an afternoon of guessing:
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()
super_debug=True plus get_stats() is the combination to reach for first when resolution misbehaves: the stats tell you what was found and what rate it resolved at, and the debug log shows you the candidates the matcher considered and rejected.
Best Practices¶
1. Start with Automatic Discovery¶
Let the patterns do their job first; fall back to a manual map only when discovery genuinely can't figure it out:
# 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¶
When the RTL names things its own way, say so explicitly rather than hoping the patterns stretch:
# 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¶
The prefix here must match what the cocotb Bus prepends — a mismatch is the single most common cause of "the signals exist but resolution failed":
# 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¶
After apply_to_component(), assert the attributes your code is about to drive. Fail at setup, not mid-transaction:
# 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¶
The stats distinguish "didn't find it" from "found two and couldn't pick" — very different problems:
# 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']}")
SignalResolver is the quiet piece that makes everything else in the framework possible: the data strategies cache the handles it finds, the BFMs drive through the attributes it applies, and the error messages it produces are written for the person debugging a bring-up at midnight. Get the mapping right once, here, and the rest of the stack never has to think about port names again.