|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
protocol_error_handler.py¶
Generic error handler that can be used with various protocol implementations to inject errors at specific addresses or address ranges. This module provides fine-grained control over error injection for testing error handling capabilities.
Overview¶
The protocol_error_handler.py module provides the ErrorHandler class, which manages error responses for specific addresses, ID values, or address ranges. This allows comprehensive testing of error handling in verification environments by simulating various error conditions that may occur in real systems.
Core Class¶
ErrorHandler¶
Error response handler for protocol transactions that manages error regions and individual error transactions.
Constructor¶
Creates a new ErrorHandler instance with empty error regions and transactions.
Core Data Structures¶
error_regions: List of address ranges that should return errorserror_transactions: Dictionary mapping (address, ID) pairs to response codesstats: Dictionary tracking error registration and trigger statistics
Response Codes¶
Standard response codes used across protocols: - 0: OKAY - Normal successful completion - 1: EXOKAY - Exclusive access success (protocol-specific) - 2: SLVERR - Slave error (default) - 3: DECERR - Decode error
Error Region Management¶
register_error_region(start_address, end_address, response_code=2)¶
Register a memory region that should return errors.
Parameters:
- start_address: Start address of the region (inclusive)
- end_address: End address of the region (inclusive)
- response_code: Error response code (default: 2/SLVERR)
# Register error regions
error_handler.register_error_region(0x8000, 0x8FFF, response_code=2) # SLVERR
error_handler.register_error_region(0x9000, 0x9FFF, response_code=3) # DECERR
# Register single address as region
error_handler.register_error_region(0xDEAD, 0xDEAD, response_code=2)
clear_error_regions()¶
Clear all registered error regions.
Individual Transaction Errors¶
register_error_transaction(address, id_value=None, response_code=2)¶
Register a specific address/ID combination for error response.
Parameters:
- address: Target address
- id_value: Transaction ID (None for any ID)
- response_code: Error response code (default: 2/SLVERR)
# Error for specific address regardless of ID
error_handler.register_error_transaction(0x1000, response_code=2)
# Error for specific address and ID combination
error_handler.register_error_transaction(0x2000, id_value=5, response_code=3)
# Multiple specific transactions
addresses = [0x3000, 0x3004, 0x3008, 0x300C]
for addr in addresses:
error_handler.register_error_transaction(addr, response_code=2)
clear_error_transactions()¶
Clear all registered error transactions.
clear_all_errors()¶
Clear all registered errors (both regions and transactions).
Error Checking¶
check_for_error(address, id_value=None)¶
Check if a transaction should return an error.
Parameters:
- address: Target address
- id_value: Transaction ID (optional)
Returns: Tuple of (should_error, response_code)
# Check for error
should_error, response_code = error_handler.check_for_error(0x8500)
if should_error:
print(f"Address 0x8500 should return error code {response_code}")
# Check with ID
should_error, response_code = error_handler.check_for_error(0x2000, id_value=5)
if should_error:
print(f"Transaction to 0x2000 with ID 5 should return error {response_code}")
Statistics¶
get_stats()¶
Get statistics about error regions and transactions.
Returns: Dictionary with statistics
stats = error_handler.get_stats()
print(f"Error regions registered: {stats['error_regions_registered']}")
print(f"Error transactions registered: {stats['error_transactions_registered']}")
print(f"Errors triggered: {stats['errors_triggered']}")
Usage Patterns¶
Basic Error Injection¶
class ErrorInjectingSlave:
def __init__(self, dut):
self.dut = dut
self.error_handler = ErrorHandler()
self.memory = {}
# Set up error regions for testing
self.setup_error_regions()
def setup_error_regions(self):
"""Configure error regions for comprehensive testing"""
# Decode error region (unmapped addresses)
self.error_handler.register_error_region(0xF000, 0xFFFF, response_code=3)
# Slave error region (mapped but faulty)
self.error_handler.register_error_region(0xE000, 0xEFFF, response_code=2)
# Specific problematic addresses
problematic_addresses = [0x1000, 0x2000, 0x3000]
for addr in problematic_addresses:
self.error_handler.register_error_transaction(addr, response_code=2)
@cocotb.coroutine
def handle_transaction(self, packet):
"""Handle transaction with error injection"""
address = packet.addr
# Check for configured errors
should_error, error_code = self.error_handler.check_for_error(address)
if should_error:
# Return error response
self.log.info(f"Injecting error {error_code} for address 0x{address:X}")
packet.resp = error_code
yield self.send_error_response(packet)
else:
# Normal processing
yield self.process_normal_transaction(packet)
@cocotb.coroutine
def process_normal_transaction(self, packet):
"""Process normal transaction"""
if packet.cmd == 1: # READ
packet.data = self.memory.get(packet.addr, 0)
packet.resp = 0 # OKAY
elif packet.cmd == 2: # WRITE
self.memory[packet.addr] = packet.data
packet.resp = 0 # OKAY
yield self.send_response(packet)
@cocotb.coroutine
def send_error_response(self, packet):
"""Send error response"""
# Set error response
self.dut.resp.value = packet.resp
self.dut.resp_valid.value = 1
yield RisingEdge(self.dut.clk)
self.dut.resp_valid.value = 0
Dynamic Error Configuration¶
class DynamicErrorManager:
def __init__(self):
self.error_handler = ErrorHandler()
self.test_phase = "normal"
def configure_for_test_phase(self, phase):
"""Configure errors based on test phase"""
self.test_phase = phase
self.error_handler.clear_all_errors()
if phase == "decode_error_test":
self._setup_decode_errors()
elif phase == "slave_error_test":
self._setup_slave_errors()
elif phase == "random_error_test":
self._setup_random_errors()
elif phase == "stress_test":
self._setup_stress_errors()
def _setup_decode_errors(self):
"""Set up decode error testing"""
# Test various unmapped regions
decode_regions = [
(0xF000, 0xF0FF), # Small unmapped region
(0xFF00, 0xFFFF), # End of address space
(0x0, 0xFF), # Beginning of address space
]
for start, end in decode_regions:
self.error_handler.register_error_region(start, end, response_code=3)
def _setup_slave_errors(self):
"""Set up slave error testing"""
# Test specific addresses that should cause slave errors
slave_error_addresses = [
0x1000, 0x1004, 0x1008, 0x100C, # Faulty memory bank
0x2000, 0x2004, # Faulty registers
0x3000, # Single faulty location
]
for addr in slave_error_addresses:
self.error_handler.register_error_transaction(addr, response_code=2)
def _setup_random_errors(self):
"""Set up random error injection"""
import random
# Random error addresses
for _ in range(20):
addr = random.randint(0x4000, 0x7FFF)
error_type = random.choice([2, 3]) # SLVERR or DECERR
self.error_handler.register_error_transaction(addr, response_code=error_type)
def _setup_stress_errors(self):
"""Set up stress testing with many errors"""
# Large error region
self.error_handler.register_error_region(0x8000, 0xBFFF, response_code=2)
# Scattered individual errors
for addr in range(0x1000, 0x2000, 16):
self.error_handler.register_error_transaction(addr, response_code=3)
def get_current_configuration(self):
"""Get current error configuration summary"""
stats = self.error_handler.get_stats()
return {
'test_phase': self.test_phase,
'error_regions': stats['error_regions_registered'],
'error_transactions': stats['error_transactions_registered'],
'total_errors_configured': stats['error_regions_registered'] + stats['error_transactions_registered']
}
AXI Error Handler Example¶
class AXIErrorHandler:
def __init__(self):
self.error_handler = ErrorHandler()
self.outstanding_errors = {} # Track multi-beat transaction errors
def setup_axi_error_scenarios(self):
"""Set up AXI-specific error scenarios"""
# Address decode errors
self.error_handler.register_error_region(0x80000000, 0x8FFFFFFF, response_code=3) # DECERR
# Slave errors for protected regions
self.error_handler.register_error_region(0x70000000, 0x7FFFFFFF, response_code=2) # SLVERR
# ID-specific errors for testing multiple outstanding transactions
for transaction_id in [5, 10, 15]:
self.error_handler.register_error_transaction(
0x60000000, id_value=transaction_id, response_code=2
)
def check_write_address_error(self, awaddr, awid):
"""Check for write address errors"""
should_error, error_code = self.error_handler.check_for_error(awaddr, awid)
if should_error:
# Store error for write response
self.outstanding_errors[awid] = error_code
self.log.info(f"Write address error: AWID={awid}, AWADDR=0x{awaddr:X}, Error={error_code}")
return should_error, error_code
def check_read_address_error(self, araddr, arid):
"""Check for read address errors"""
should_error, error_code = self.error_handler.check_for_error(araddr, arid)
if should_error:
# Store error for read data response
self.outstanding_errors[arid] = error_code
self.log.info(f"Read address error: ARID={arid}, ARADDR=0x{araddr:X}, Error={error_code}")
return should_error, error_code
def get_write_response_error(self, bid):
"""Get write response error code"""
if bid in self.outstanding_errors:
error_code = self.outstanding_errors.pop(bid)
return True, error_code
return False, 0
def get_read_data_error(self, rid):
"""Get read data error code"""
if rid in self.outstanding_errors:
error_code = self.outstanding_errors[rid]
# Don't remove yet - might be multi-beat
return True, error_code
return False, 0
def clear_transaction_error(self, transaction_id):
"""Clear error for completed transaction"""
if transaction_id in self.outstanding_errors:
del self.outstanding_errors[transaction_id]
Test Framework Integration¶
@cocotb.test()
def error_injection_test(dut):
"""Comprehensive error injection testing"""
# Set up error handler
error_handler = ErrorHandler()
# Configure error scenarios
test_scenarios = [
{
'name': 'decode_errors',
'regions': [(0xF000, 0xFFFF, 3)],
'transactions': []
},
{
'name': 'slave_errors',
'regions': [(0xE000, 0xEFFF, 2)],
'transactions': [(0x1000, None, 2), (0x2000, None, 2)]
},
{
'name': 'mixed_errors',
'regions': [(0xD000, 0xDFFF, 3)],
'transactions': [(0x3000, 5, 2), (0x4000, 10, 3)]
}
]
for scenario in test_scenarios:
cocotb.log.info(f"Testing scenario: {scenario['name']}")
# Clear previous configuration
error_handler.clear_all_errors()
# Configure error regions
for start, end, code in scenario['regions']:
error_handler.register_error_region(start, end, code)
# Configure error transactions
for addr, id_val, code in scenario['transactions']:
error_handler.register_error_transaction(addr, id_val, code)
# Run test with this configuration
yield run_error_test_sequence(dut, error_handler, scenario['name'])
# Validate error statistics
stats = error_handler.get_stats()
assert stats['errors_triggered'] > 0, f"No errors triggered in {scenario['name']}"
cocotb.log.info(f"Scenario {scenario['name']}: {stats['errors_triggered']} errors triggered")
@cocotb.coroutine
def run_error_test_sequence(dut, error_handler, scenario_name):
"""Run test sequence with error checking"""
# Test addresses that should cause errors
test_addresses = [0xF000, 0xE500, 0x1000, 0x2000, 0x3000, 0x4000]
test_ids = [0, 5, 10, 15]
for addr in test_addresses:
for test_id in test_ids:
# Check if this should cause an error
should_error, error_code = error_handler.check_for_error(addr, test_id)
# Drive transaction
yield drive_transaction(dut, addr, test_id)
# Check response
response = yield capture_response(dut)
if should_error:
assert response.error_code == error_code, \
f"Expected error {error_code}, got {response.error_code} for addr=0x{addr:X}, id={test_id}"
cocotb.log.info(f"Correctly received error {error_code} for 0x{addr:X}")
else:
assert response.error_code == 0, \
f"Unexpected error {response.error_code} for addr=0x{addr:X}, id={test_id}"
@cocotb.coroutine
def drive_transaction(dut, addr, transaction_id):
"""Drive transaction to DUT"""
dut.addr.value = addr
dut.id.value = transaction_id
dut.valid.value = 1
yield RisingEdge(dut.clk)
dut.valid.value = 0
@cocotb.coroutine
def capture_response(dut):
"""Capture response from DUT"""
yield RisingEdge(dut.response_valid)
response = type('Response', (), {})()
response.error_code = int(dut.response_code.value)
response.id = int(dut.response_id.value)
return response
Advanced Error Patterns¶
class AdvancedErrorPatterns:
def __init__(self):
self.error_handler = ErrorHandler()
self.error_patterns = {}
def create_intermittent_errors(self, address, error_rate=0.1):
"""Create intermittent errors at specific address"""
import random
def intermittent_check(addr, id_val=None):
if addr == address and random.random() < error_rate:
return True, 2 # SLVERR
return False, 0
# Store custom pattern
self.error_patterns[f'intermittent_{address:X}'] = intermittent_check
def create_burst_errors(self, base_address, burst_length):
"""Create errors for burst transactions"""
# Register errors for entire burst range
end_address = base_address + (burst_length * 4) - 1
self.error_handler.register_error_region(base_address, end_address, response_code=2)
def create_id_based_errors(self, problematic_ids):
"""Create errors based on transaction IDs"""
for transaction_id in problematic_ids:
# Error for any address with this ID
for addr in range(0x1000, 0x2000, 4):
self.error_handler.register_error_transaction(addr, transaction_id, response_code=2)
def create_temporal_errors(self, start_time, end_time, addresses):
"""Create time-based error injection"""
# This would require integration with simulation time
# Store temporal error configuration
self.temporal_errors = {
'start_time': start_time,
'end_time': end_time,
'addresses': addresses
}
def check_advanced_error(self, address, id_value=None, current_time=None):
"""Check for advanced error patterns"""
# Check standard error handler first
should_error, error_code = self.error_handler.check_for_error(address, id_value)
if should_error:
return should_error, error_code
# Check custom patterns
for pattern_name, pattern_func in self.error_patterns.items():
should_error, error_code = pattern_func(address, id_value)
if should_error:
return should_error, error_code
# Check temporal errors
if hasattr(self, 'temporal_errors') and current_time:
temporal = self.temporal_errors
if (temporal['start_time'] <= current_time <= temporal['end_time'] and
address in temporal['addresses']):
return True, 2
return False, 0
Error Analysis and Reporting¶
class ErrorAnalyzer:
def __init__(self, error_handler):
self.error_handler = error_handler
self.error_log = []
def log_error_event(self, address, id_value, error_code, timestamp):
"""Log error event for analysis"""
self.error_log.append({
'address': address,
'id': id_value,
'error_code': error_code,
'timestamp': timestamp,
'error_type': self._get_error_type_name(error_code)
})
def _get_error_type_name(self, error_code):
"""Convert error code to readable name"""
error_names = {0: 'OKAY', 1: 'EXOKAY', 2: 'SLVERR', 3: 'DECERR'}
return error_names.get(error_code, f'UNKNOWN_{error_code}')
def generate_error_report(self):
"""Generate comprehensive error analysis report"""
stats = self.error_handler.get_stats()
# Analyze error distribution
error_types = {}
address_patterns = {}
for event in self.error_log:
# Count error types
error_type = event['error_type']
error_types[error_type] = error_types.get(error_type, 0) + 1
# Count address patterns
addr_range = (event['address'] // 0x1000) * 0x1000 # Group by 4KB
address_patterns[addr_range] = address_patterns.get(addr_range, 0) + 1
report = {
'configuration_stats': stats,
'runtime_stats': {
'total_errors_logged': len(self.error_log),
'error_type_distribution': error_types,
'address_pattern_distribution': address_patterns
},
'coverage_analysis': self._analyze_error_coverage()
}
return report
def _analyze_error_coverage(self):
"""Analyze how well error scenarios were exercised"""
# Check if configured errors were actually triggered
# This would require tracking which specific configurations caused errors
return {
'configured_regions_hit': "Analysis would go here",
'configured_transactions_hit': "Analysis would go here",
'coverage_percentage': 85.5 # Example
}
Best Practices¶
1. Organize Errors by Test Phase¶
def setup_test_phase_errors(error_handler, phase):
error_handler.clear_all_errors()
if phase == "basic_functionality":
# Minimal errors for basic testing
pass
elif phase == "error_handling":
# Comprehensive error scenarios
setup_comprehensive_errors(error_handler)
elif phase == "stress_testing":
# Heavy error injection
setup_stress_errors(error_handler)
2. Use Appropriate Error Types¶
# Decode errors for unmapped addresses
error_handler.register_error_region(0xF000, 0xFFFF, response_code=3) # DECERR
# Slave errors for mapped but faulty regions
error_handler.register_error_region(0xE000, 0xEFFF, response_code=2) # SLVERR
3. Test Both Region and Transaction Errors¶
# Test broad regions
error_handler.register_error_region(0x8000, 0x8FFF, response_code=2)
# Test specific transactions
error_handler.register_error_transaction(0x1000, response_code=3)
4. Monitor Error Statistics¶
# Regular statistics monitoring
def check_error_coverage():
stats = error_handler.get_stats()
if stats['errors_triggered'] == 0:
log.warning("No errors have been triggered yet")
5. Clear Errors Between Test Phases¶
# Always clear errors when changing test scenarios
error_handler.clear_all_errors()
setup_new_test_errors()
The ErrorHandler provides a comprehensive framework for testing error handling capabilities across different protocols, enabling thorough validation of error response mechanisms in verification environments.