|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
gaxi_monitor.py¶
The passive observer. A GAXIMonitor never drives a pin — it watches valid and ready, timestamps the handshake, unpacks the fields, and drops a packet into the queue for your callbacks. The one real decision is which side of the interface to watch (is_slave), because that choice changes when the data gets sampled.
Overview¶
The GAXIMonitor class provides:
- Pure observation — it drives nothing, ever
- Master-side or slave-side monitoring via the is_slave parameter
- Mode-aware data sampling (the fifo_flop case below is the one that bites people)
- Protocol violation and X/Z tracking in the statistics
- Everything else inherited from GAXIMonitorBase: signal resolution, packet building, memory integration
Inherits all common functionality from GAXIMonitorBase including signal resolution, data collection, packet management, and memory integration.
Class¶
GAXIMonitor¶
class GAXIMonitor(GAXIMonitorBase):
def __init__(self, dut, title, prefix, clock, field_config, is_slave=False,
mode='skid', bus_name='', pkt_prefix='', multi_sig=False,
log=None, super_debug=False, signal_map=None,
protocol_type=None, **kwargs)
Parameters:
- dut: Device under test
- title: Component title/name
- prefix: Bus prefix
- clock: Clock signal
- field_config: Field configuration
- is_slave: If True, monitor slave side; if False, monitor master side
- mode: GAXI mode ('skid', 'fifo_mux', 'fifo_flop') - for DUT parameter only
- bus_name: Bus/channel name
- pkt_prefix: Packet field prefix
- multi_sig: Whether using multi-signal mode
- log: Logger instance (required — raises ValueError if None; pass your TBBase logger)
- super_debug: Enable detailed debugging
- signal_map: Optional manual signal mapping override
- **kwargs: Additional arguments
Data Sampling Rules¶
Sampling timing depends on the side and the mode, and getting it wrong looks like off-by-one data corruption in the DUT — so get it right here instead.
Master Side Monitoring (is_slave=False)¶
- All modes: sample the moment the handshake completes
- Master-side signals are stable at handshake time, so waiting gains nothing
- Lowest-latency observation
Slave Side Monitoring (is_slave=True)¶
- Skid mode: sample at the handshake
- FIFO MUX mode: sample at the handshake
- FIFO FLOP mode: wait one cycle after the handshake, then capture
- The delayed capture matches the DUT: a registered FIFO output doesn't show the new data until the cycle after the handshake. Sample early and you record whatever was on the bus before.
Core Methods¶
Inherited Methods¶
GAXIMonitor inherits all methods from GAXIMonitorBase:
- create_packet(**field_values) - Create packets
- get_observed_packets(count=None) - Get observed transactions
- clear_queue() - Clear observation queue
- handle_memory_write(packet) - Memory integration
- handle_memory_read(packet) - Memory integration
- get_base_stats() - Statistics collection
Monitor-Specific Methods¶
get_stats()¶
Base statistics plus the monitor-specific ones: which side you're watching, observed count, violations.
Returns: Dictionary containing all statistics
stats = monitor.get_stats()
print(f"Monitor type: {stats['monitor_type']}") # 'master' or 'slave'
print(f"Observed packets: {stats['observed_packets']}")
print(f"Protocol violations: {stats['monitor_stats']['protocol_violations']}")
Usage Patterns¶
Basic Master Side Monitoring¶
import cocotb
from cocotb.triggers import Timer
from CocoTBFramework.components.gaxi import GAXIMonitor
from CocoTBFramework.components.shared.field_config import FieldConfig
@cocotb.test()
async def test_master_monitoring(dut):
clock = dut.clk
# Create field configuration
field_config = FieldConfig()
field_config.add_field(FieldDefinition("addr", 32, format="hex"))
field_config.add_field(FieldDefinition("data", 32, format="hex"))
# Create master side monitor
master_monitor = GAXIMonitor(
dut=dut,
title="MasterMonitor",
prefix="m_", # Monitor master interface
clock=clock,
field_config=field_config,
is_slave=False, # Monitor master side
mode='skid',
log=log # Required
)
# Add callback to process observed transactions
def log_transaction(packet):
print(f"Master sent: addr=0x{packet.addr:X}, data=0x{packet.data:X}")
master_monitor.add_callback(log_transaction)
# Run test and let monitor observe
await Timer(1000, units='ns')
# Check observed transactions
packets = master_monitor.get_observed_packets()
print(f"Master monitor observed {len(packets)} transactions")
Basic Slave Side Monitoring¶
@cocotb.test()
async def test_slave_monitoring(dut):
clock = dut.clk
# Create field configuration
field_config = FieldConfig()
field_config.add_field(FieldDefinition("addr", 32, format="hex"))
field_config.add_field(FieldDefinition("data", 32, format="hex"))
# Create slave side monitor with FIFO FLOP mode
slave_monitor = GAXIMonitor(
dut=dut,
title="SlaveMonitor",
prefix="s_", # Monitor slave interface
clock=clock,
field_config=field_config,
is_slave=True, # Monitor slave side
mode='fifo_flop' # DUT uses registered interface
)
# Add callback to process observed transactions
def process_slave_transaction(packet):
print(f"Slave received: addr=0x{packet.addr:X}, data=0x{packet.data:X}")
# Additional processing
if packet.addr >= 0x8000:
print(" → High address region")
slave_monitor.add_callback(process_slave_transaction)
# Run test
await Timer(1000, units='ns')
# Analyze slave activity
packets = slave_monitor.get_observed_packets()
print(f"Slave monitor observed {len(packets)} transactions")
Dual Monitor Setup¶
Monitors are cheap. Watch both sides and let the counts disagree if something drops a transaction.
@cocotb.test()
async def test_dual_monitoring(dut):
clock = dut.clk
field_config = create_field_config()
# Monitor both sides of the interface
master_monitor = GAXIMonitor(
dut=dut,
title="MasterSideMonitor",
prefix="m_",
clock=clock,
field_config=field_config,
is_slave=False # Master side
)
slave_monitor = GAXIMonitor(
dut=dut,
title="SlaveSideMonitor",
prefix="s_",
clock=clock,
field_config=field_config,
is_slave=True # Slave side
)
# Track transactions on both sides
master_transactions = []
slave_transactions = []
def track_master(packet):
master_transactions.append(packet)
print(f"Master → Slave: {packet.formatted(compact=True)}")
def track_slave(packet):
slave_transactions.append(packet)
print(f"Slave ← Master: {packet.formatted(compact=True)}")
master_monitor.add_callback(track_master)
slave_monitor.add_callback(track_slave)
# Run test
await Timer(2000, units='ns')
# Compare transaction counts
print(f"Master side: {len(master_transactions)} transactions")
print(f"Slave side: {len(slave_transactions)} transactions")
# Verify transaction matching
assert len(master_transactions) == len(slave_transactions), \
"Transaction count mismatch between master and slave sides"
Protocol Violation Monitoring¶
@cocotb.test()
async def test_protocol_violations(dut):
# Create monitor with protocol checking
monitor = GAXIMonitor(
dut=dut,
title="ProtocolMonitor",
prefix="",
clock=clock,
field_config=field_config,
is_slave=False,
super_debug=True # Enable detailed logging
)
# Track protocol violations
violations = []
def check_protocol(packet):
"""Check for protocol violations in observed transactions"""
# Check address alignment
if hasattr(packet, 'addr') and packet.addr % 4 != 0:
violation = f"Unaligned address: 0x{packet.addr:X}"
violations.append(violation)
print(f"VIOLATION: {violation}")
# Check for X/Z values
for field_name in ['addr', 'data']:
if hasattr(packet, field_name):
value = getattr(packet, field_name)
if value == -1: # X/Z represented as -1
violation = f"X/Z value in {field_name}"
violations.append(violation)
print(f"VIOLATION: {violation}")
monitor.add_callback(check_protocol)
# Run test
await Timer(1000, units='ns')
# Report violations
if violations:
print(f"Detected {len(violations)} protocol violations:")
for violation in violations:
print(f" - {violation}")
else:
print("No protocol violations detected")
# Get monitor statistics
stats = monitor.get_stats()
print(f"Monitor statistics: {stats['monitor_stats']}")
Performance Monitoring¶
Interval and throughput tracking from the callback:
@cocotb.test()
async def test_performance_monitoring(dut):
monitor = GAXIMonitor(dut, "PerfMonitor", "", clock, field_config)
# Performance tracking
transaction_times = []
inter_transaction_times = []
last_time = 0
def track_performance(packet):
nonlocal last_time
current_time = get_sim_time('ns')
transaction_times.append(current_time)
if last_time > 0:
inter_time = current_time - last_time
inter_transaction_times.append(inter_time)
last_time = current_time
monitor.add_callback(track_performance)
# Run test
await Timer(5000, units='ns')
# Analyze performance
if inter_transaction_times:
avg_interval = sum(inter_transaction_times) / len(inter_transaction_times)
throughput = 1e9 / avg_interval if avg_interval > 0 else 0
print(f"Performance Analysis:")
print(f" Transactions: {len(transaction_times)}")
print(f" Average interval: {avg_interval:.2f} ns")
print(f" Throughput: {throughput:.1f} TPS")
print(f" Min interval: {min(inter_transaction_times):.2f} ns")
print(f" Max interval: {max(inter_transaction_times):.2f} ns")
Mode-Specific Configuration¶
def create_mode_specific_monitors(dut, clock, field_config):
"""Create monitors for different DUT modes"""
monitors = {}
# Skid mode monitor (immediate capture)
monitors['skid'] = GAXIMonitor(
dut=dut,
title="SkidMonitor",
prefix="skid_",
clock=clock,
field_config=field_config,
is_slave=True,
mode='skid'
)
# FIFO MUX mode monitor (immediate capture)
monitors['fifo_mux'] = GAXIMonitor(
dut=dut,
title="FifoMuxMonitor",
prefix="mux_",
clock=clock,
field_config=field_config,
is_slave=True,
mode='fifo_mux'
)
# FIFO FLOP mode monitor (delayed capture)
monitors['fifo_flop'] = GAXIMonitor(
dut=dut,
title="FifoFlopMonitor",
prefix="flop_",
clock=clock,
field_config=field_config,
is_slave=True,
mode='fifo_flop' # One cycle delay for data capture
)
return monitors
@cocotb.test()
async def test_mode_specific_monitoring(dut):
monitors = create_mode_specific_monitors(dut, clock, field_config)
# Add same callback to all monitors
def log_transaction(packet):
print(f"Transaction: {packet.formatted(compact=True)}")
for mode, monitor in monitors.items():
monitor.add_callback(log_transaction)
# Run test
await Timer(1000, units='ns')
# Compare results across modes
for mode, monitor in monitors.items():
packets = monitor.get_observed_packets()
stats = monitor.get_stats()
print(f"{mode} mode: {len(packets)} packets, {stats['monitor_type']} side")
Integration with Scoreboards¶
This is the standard wiring: expected from the master side, actual from the slave side, scoreboard does the comparison.
from CocoTBFramework.scoreboards.gaxi_scoreboard import GAXIScoreboard
@cocotb.test()
async def test_scoreboard_integration(dut):
# Create scoreboard
scoreboard = GAXIScoreboard("TestScoreboard", field_config, log=log)
# Create monitors
master_monitor = GAXIMonitor(dut, "MasterMon", "m_", clock, field_config,
is_slave=False)
slave_monitor = GAXIMonitor(dut, "SlaveMon", "s_", clock, field_config,
is_slave=True)
# Connect monitors to scoreboard
master_monitor.add_callback(scoreboard.add_expected)
slave_monitor.add_callback(scoreboard.add_actual)
# Run test
await Timer(2000, units='ns')
# Check scoreboard results
error_count = scoreboard.report()
print(f"Scoreboard: {scoreboard.transaction_count} transactions compared, "
f"{error_count} errors")
# Verify all transactions matched
assert error_count == 0, "Transaction mismatches detected"
Memory Validation Monitoring¶
@cocotb.test()
async def test_memory_validation(dut):
# Create memory model for validation
memory = MemoryModel(num_lines=256, bytes_per_line=4, log=log)
# Create monitor with memory integration
monitor = GAXIMonitor(
dut=dut,
title="MemoryMonitor",
prefix="",
clock=clock,
field_config=field_config,
memory_model=memory,
is_slave=True
)
# Track memory operations
def validate_memory_transaction(packet):
"""Validate transactions against memory model"""
if hasattr(packet, 'cmd'):
if packet.cmd == 2: # Write
success = monitor.handle_memory_write(packet)
if success:
print(f"Memory write: addr=0x{packet.addr:X}, "
f"data=0x{packet.data:X}")
else:
print(f"Memory write failed: addr=0x{packet.addr:X}")
elif packet.cmd == 1: # Read
success, data = monitor.handle_memory_read(packet)
if success:
print(f"Memory read: addr=0x{packet.addr:X}, "
f"data=0x{data:X}")
# Validate read data if available
if hasattr(packet, 'data') and packet.data != data:
print(f" WARNING: Expected 0x{data:X}, "
f"got 0x{packet.data:X}")
monitor.add_callback(validate_memory_transaction)
# Run test
await Timer(1000, units='ns')
# Get memory statistics
stats = monitor.get_stats()
if 'memory_stats' in stats:
memory_stats = stats['memory_stats']
print(f"Memory operations: reads={memory_stats['reads']}, "
f"writes={memory_stats['writes']}")
Advanced Features¶
Custom Signal Mapping¶
# For non-standard signal names
signal_map = {
'valid': 'master_transaction_valid',
'ready': 'slave_ready_signal',
'data': 'transaction_data_bus'
}
monitor = GAXIMonitor(
dut=dut,
title="CustomMonitor",
prefix="",
clock=clock,
field_config=field_config,
signal_map=signal_map # Override automatic discovery
)
Multi-Field Monitoring¶
# Create field configuration with multiple fields
field_config = FieldConfig()
field_config.add_field(FieldDefinition("addr", 32, format="hex"))
field_config.add_field(FieldDefinition("data", 32, format="hex"))
field_config.add_field(FieldDefinition("cmd", 4, format="hex"))
field_config.add_field(FieldDefinition("id", 8, format="hex"))
# Monitor with multi-signal mode
monitor = GAXIMonitor(
dut=dut,
title="MultiFieldMonitor",
prefix="",
clock=clock,
field_config=field_config,
multi_sig=True # Individual signals for each field
)
Error Handling¶
Signal Resolution Errors¶
try:
monitor = GAXIMonitor(dut, "Monitor", "", clock, field_config)
except RuntimeError as e:
print(f"Signal resolution failed: {e}")
# Try with manual signal mapping
signal_map = create_manual_signal_map()
monitor = GAXIMonitor(dut, "Monitor", "", clock, field_config,
signal_map=signal_map)
Monitoring Errors¶
Signal problems don't stop the monitor — they accumulate in the statistics. Check there first when counts look off.
# Monitor automatically handles signal errors
# Check statistics for error information
stats = monitor.get_stats()
monitor_stats = stats['monitor_stats']
if monitor_stats['x_z_violations'] > 0:
print(f"X/Z violations detected: {monitor_stats['x_z_violations']}")
Best Practices¶
1. Choose the Side That Answers Your Question¶
# Monitor master side to see outgoing transactions
master_monitor = GAXIMonitor(..., is_slave=False)
# Monitor slave side to see received transactions
slave_monitor = GAXIMonitor(..., is_slave=True)
2. Match Mode to the DUT Implementation¶
This is the configuration error that wastes afternoons. Registered interface on the DUT means fifo_flop on the monitor, or every packet looks shifted.
# For DUT with registered slave interface
monitor = GAXIMonitor(..., is_slave=True, mode='fifo_flop')
# For DUT with combinational interface
monitor = GAXIMonitor(..., is_slave=True, mode='skid')
3. Use Callbacks, Not Polling¶
# Prefer callbacks over polling
monitor.add_callback(process_transaction)
# Avoid polling the queue directly
# while True:
# if monitor._recvQ: # Don't do this
packets = monitor.get_observed_packets() # Do this instead
4. Enable Debug During Development¶
monitor = GAXIMonitor(..., super_debug=True) # Development
monitor = GAXIMonitor(..., super_debug=False) # Production
5. Check the Violation Counters¶
# Regular statistics checking
stats = monitor.get_stats()
if stats['monitor_stats']['protocol_violations'] > 0:
print("Protocol violations detected - investigate")
Put a monitor on each side, wire them into a scoreboard, and most interface bugs announce themselves. The callback path is where the real-time checking happens; get_observed_packets() is for the post-run autopsy.