|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
axi4_scoreboard.py¶
AXI4 is where simple expected/actual queue matching runs out of road: multiple IDs, out-of-order completion, five channels. Transactions finish in whatever order the interconnect feels like. This scoreboard tracks each one by ID on both sides of the interface and pairs the master-side half with the slave-side half when both have arrived.
Overview¶
- ID-based Transaction Tracking: a separate queue per AXI4 ID
- Channel Separation: reads and writes tracked independently
- Master/Slave Monitoring: both sides report; comparison fires when the two halves meet
- Protocol Compliance: AXI4 rule checking built in
- Performance Analysis: transaction counts, timing, and throughput data
Classes¶
AXI4Scoreboard¶
Full-protocol AXI4 verification.
class AXI4Scoreboard(BaseScoreboard):
def __init__(self, name, id_width=8, addr_width=32, data_width=32, user_width=1, log=None)
Parameters:
- name: scoreboard name; appears in reports
- id_width: width of ID fields in bits (default: 8)—size this for your system's ID space
- addr_width: width of address fields in bits (default: 32)
- data_width: width of data fields in bits (default: 32)
- user_width: width of user fields in bits (default: 1)
- log: logger for the detail
Key Attributes:
- write_count: writes processed
- read_count: reads processed
- protocol_error_count: protocol violations caught
- master_writes: master-side writes, keyed by ID
- slave_writes: slave-side writes, keyed by ID
- master_reads: master-side reads, keyed by ID
- slave_reads: slave-side reads, keyed by ID
Monitor Integration¶
The scoreboard speaks two callback dialects and uses whichever the monitor offers:
- Custom monitors exposing
set_write_callback(cb)/set_read_callback(cb). Callbacks are invoked with(id_value, transaction). When a monitor provides both methods, they are preferred. - Framework monitors (
GAXIMonitor, anycocotb_busMonitor/BusMonitorsubclass) exposingadd_callback(cb). Callbacks are invoked with(transaction,); the scoreboard classifies each transaction as a read or a write from its composite keys (aw_transaction/w_transactions/b_transactionvsar_transaction/r_transactions) and extracts the AXI4 ID automatically (from anidkey, or from the AW/B or AR/R packets).
A monitor offering neither raises ValueError when you connect it. A transaction that can't be classified as read or write gets logged and ignored.
Transaction format¶
Transactions arrive as composite dictionaries—one per AXI4 transaction, holding the channel packets:
write_tx = {
'aw_transaction': aw_packet, # AW channel packet
'w_transactions': [w_packet, ...], # W channel packets (one per beat)
'b_transaction': b_packet, # B channel packet
}
read_tx = {
'ar_transaction': ar_packet,
'r_transactions': [r_packet, ...],
}
Channel packets may use either naming dialect: AXI-prefixed (awaddr, awlen,
wdata, bresp, rdata, ...) or the framework's generic names
(addr, len, data, resp, ...). Field access tries both, so the master and
slave sides don't even have to agree on naming. Packets can be objects
(attribute access) or dictionaries (key access)—both work.
Master Monitor Connection¶
add_master_monitor(monitor)¶
Hook up the monitor watching the master interface.
Parameters:
- monitor: monitor instance observing the master interface
Behavior:
- Registers write/read handlers via set_write_callback/set_read_callback
when available, otherwise via add_callback
- From then on, master-side transactions flow in automatically
# Connect master monitor
axi4_scoreboard.add_master_monitor(master_monitor)
# Monitor automatically feeds transactions to scoreboard
Slave Monitor Connection¶
add_slave_monitor(monitor)¶
Same deal on the slave side.
Parameters:
- monitor: monitor instance observing the slave interface
Behavior:
- Registers write/read handlers via set_write_callback/set_read_callback
when available, otherwise via add_callback
- From then on, slave-side transactions flow in automatically
# Connect slave monitor
axi4_scoreboard.add_slave_monitor(slave_monitor)
# Complete master-slave verification setup
Transaction Processing¶
Write Transaction Handling¶
_handle_master_write(id_value, transaction)¶
A completed write showed up on the master side.
Parameters:
- id_value: AXI4 ID of the transaction
- transaction: the completed write
Behavior:
- Filed under master_writes[id_value]
- If the slave-side half is already waiting, comparison runs
- Write counters updated
_handle_slave_write(id_value, transaction)¶
A completed write showed up on the slave side.
Parameters:
- id_value: AXI4 ID of the transaction
- transaction: the completed write
Behavior:
- Filed under slave_writes[id_value]
- If the master-side half is already waiting, comparison runs
- Write response compliance validated
Read Transaction Handling¶
_handle_master_read(id_value, transaction)¶
A completed read showed up on the master side.
Parameters:
- id_value: AXI4 ID of the transaction
- transaction: the completed read
Behavior:
- Filed under master_reads[id_value]
- If the slave-side half is already waiting, comparison runs
- Read counters updated
_handle_slave_read(id_value, transaction)¶
A completed read showed up on the slave side.
Parameters:
- id_value: AXI4 ID of the transaction
- transaction: the completed read
Behavior:
- Filed under slave_reads[id_value]
- If the master-side half is already waiting, comparison runs
- Read data and response codes validated
Transaction Verification¶
Write Transaction Matching¶
_check_write_match(id_value, master_transaction, slave_transaction)¶
What has to agree before a write counts as matched.
Parameters:
- id_value: transaction ID
- master_transaction: master-side write
- slave_transaction: slave-side write
Verification Checks:
- Address field consistency (awaddr/addr)
- Burst parameters (awlen/len, awsize/size, awburst/burst)
- Data payload, beat by beat (wdata/data)
- Response code (bresp/resp)
- A field present on only one side is reported as a mismatch; a field absent
on both sides is skipped
# Example write verification
# Master: AWADDR=0x1000, WDATA=[0xDEADBEEF, 0x12345678], WSTRB=[0xF, 0xF]
# Slave: AWADDR=0x1000, WDATA=[0xDEADBEEF, 0x12345678], BRESP=OKAY
# Result: MATCH - addresses align, data matches, response is OKAY
Read Transaction Matching¶
_check_read_match(id_value, master_transaction, slave_transaction)¶
Same idea for reads.
Parameters:
- id_value: transaction ID
- master_transaction: master-side read
- slave_transaction: slave-side read
Verification Checks:
- Address field consistency (araddr/addr)
- Burst parameters (arlen/len, arsize/size, arburst/burst)
- Read data, beat by beat (rdata/data)
- A field present on only one side is reported as a mismatch; a field absent
on both sides is skipped
# Example read verification
# Master: ARADDR=0x2000, ARLEN=3, ARID=5
# Slave: ARADDR=0x2000, RDATA=[0x11, 0x22, 0x33, 0x44], RID=5, RRESP=OKAY
# Result: MATCH - address correct, data length matches, ID preserved
Protocol Compliance Checking¶
Built-in Validation¶
Checked automatically on every transaction: - ID Consistency: response IDs match request IDs - Burst Alignment: address alignment fits the burst size - Response Codes: RESP values are legal (OKAY, EXOKAY, SLVERR, DECERR) - Outstanding Limits: configurable caps on outstanding transactions per ID - Ordering Requirements: the AXI4 ordering model
Protocol Error Detection¶
Violations are logged as they happen:
# Protocol errors automatically logged:
# - Mismatched transaction IDs
# - Invalid burst parameters
# - Out-of-order responses
# - Response timeout violations
# - Invalid response codes
if axi4_scoreboard.protocol_error_count > 0:
print(f"Protocol violations detected: {axi4_scoreboard.protocol_error_count}")
Performance Analysis¶
Transaction Statistics¶
Tracked for you: - Transaction Counts: read and write totals - ID Utilization: how traffic spread across the ID space - Channel Efficiency: bandwidth utilization analysis - Latency Metrics: average response times by transaction type
Performance Reporting¶
# Access performance statistics
stats = {
'total_transactions': axi4_scoreboard.transaction_count,
'write_transactions': axi4_scoreboard.write_count,
'read_transactions': axi4_scoreboard.read_count,
'protocol_errors': axi4_scoreboard.protocol_error_count,
'id_utilization': len(axi4_scoreboard.master_writes) + len(axi4_scoreboard.master_reads)
}
print(f"AXI4 Performance: {stats['total_transactions']} transactions")
print(f"Read/Write Ratio: {stats['read_transactions']}/{stats['write_transactions']}")
print(f"Protocol Compliance: {stats['protocol_errors']} violations")
Usage Examples¶
Basic AXI4 Verification Setup¶
The standard wiring: one monitor per side, then let transactions accumulate.
from CocoTBFramework.scoreboards.axi4_scoreboard import AXI4Scoreboard
# NOTE: the framework does not ship an `AXI4Monitor` class. The monitors
# below are illustrative — any framework monitor with `add_callback()`
# (GAXIMonitor, cocotb_bus BusMonitor) or any custom object implementing
# `set_write_callback()` / `set_read_callback()` (called with
# `(id_value, transaction)`) can be connected via `add_master_monitor()` /
# `add_slave_monitor()`.
# Create scoreboard for 64-bit AXI4 with 4-bit IDs
scoreboard = AXI4Scoreboard(
name="AXI4_Memory",
id_width=4,
addr_width=64,
data_width=64,
user_width=4,
log=logger
)
# Create and connect monitors
master_monitor = AXI4Monitor(dut.master_axi, "Master", clock)
slave_monitor = AXI4Monitor(dut.slave_axi, "Slave", clock)
scoreboard.add_master_monitor(master_monitor)
scoreboard.add_slave_monitor(slave_monitor)
# Scoreboard automatically captures and verifies transactions
await Timer(1000, units='ns') # Run test
# Generate verification report
error_count = scoreboard.report()
success_rate = scoreboard.result()
print(f"AXI4 Verification: {'PASS' if error_count == 0 else 'FAIL'} ({success_rate:.2%})")
Advanced Multi-ID Verification¶
Sixteen IDs in flight at once—exactly the case per-ID tracking exists for.
# Test with multiple outstanding transactions
async def test_multi_id_axi4():
# Configure for high-performance testing
scoreboard = AXI4Scoreboard(
name="HighPerf_AXI4",
id_width=8, # 256 possible IDs
addr_width=32,
data_width=128, # Wide data bus
log=logger
)
# Connect monitors
scoreboard.add_master_monitor(master_monitor)
scoreboard.add_slave_monitor(slave_monitor)
# Generate transactions with different IDs
master = AXI4Master(dut.master_axi, clock)
# Launch multiple concurrent transactions
for i in range(16):
write_transaction = master.create_write_transaction(
addr=0x10000 + (i * 0x1000),
data=[0xDEADBEEF + i],
id=i,
burst_len=4
)
await master.send_write(write_transaction)
# Wait for completion and verify
await Timer(5000, units='ns')
# Analyze results by ID
print(f"Write transactions: {scoreboard.write_count}")
print(f"Active IDs: {len(scoreboard.master_writes)}")
print(f"Protocol errors: {scoreboard.protocol_error_count}")
Memory System Verification¶
A memory controller exercised with sequential, random, and burst traffic, checking protocol health between patterns.
# Verify AXI4 memory controller
async def test_memory_controller():
scoreboard = AXI4Scoreboard("MemCtrl", log=logger)
# Connect to memory controller interfaces
cpu_monitor = AXI4Monitor(dut.cpu_axi, "CPU", clock)
ddr_monitor = AXI4Monitor(dut.ddr_axi, "DDR", clock)
scoreboard.add_master_monitor(cpu_monitor)
scoreboard.add_slave_monitor(ddr_monitor)
# Generate realistic memory access patterns
cpu_master = AXI4Master(dut.cpu_axi, clock)
# Test different access patterns
patterns = [
# Sequential reads
[(0x0000 + i*8, 'READ') for i in range(64)],
# Random writes
[(random.randint(0x1000, 0x2000) & ~7, 'WRITE') for _ in range(32)],
# Burst transfers
[(0x3000 + i*64, 'BURST_READ', 8) for i in range(8)]
]
for pattern in patterns:
for access in pattern:
if access[1] == 'READ':
await cpu_master.read(access[0], id=random.randint(0, 15))
elif access[1] == 'WRITE':
await cpu_master.write(access[0], random.randint(0, 0xFFFFFFFF), id=random.randint(0, 15))
elif access[1] == 'BURST_READ':
await cpu_master.burst_read(access[0], access[2], id=random.randint(0, 15))
# Check intermediate results
if scoreboard.protocol_error_count > 0:
print(f"Protocol errors after pattern: {scoreboard.protocol_error_count}")
break
# Final verification
final_errors = scoreboard.report()
print(f"Memory controller verification: {final_errors} errors")
Cross-Clock Domain Verification¶
Clock crossings add latency the scoreboard can't see through; this pattern tracks transactions across the boundary and compares with CDC delay in mind.
# Verify AXI4 clock domain crossing
async def test_clock_domain_crossing():
# Separate scoreboards for each clock domain
fast_scoreboard = AXI4Scoreboard("FastDomain", log=logger)
slow_scoreboard = AXI4Scoreboard("SlowDomain", log=logger)
# Connect monitors on both sides of CDC
fast_monitor = AXI4Monitor(dut.fast_axi, "Fast", fast_clock)
slow_monitor = AXI4Monitor(dut.slow_axi, "Slow", slow_clock)
fast_scoreboard.add_master_monitor(fast_monitor)
slow_scoreboard.add_slave_monitor(slow_monitor)
# Create cross-domain transaction tracker
class CDCTracker:
def __init__(self):
self.fast_transactions = {}
self.slow_transactions = {}
def on_fast_transaction(self, id_val, transaction):
self.fast_transactions[id_val] = transaction
self.check_matching()
def on_slow_transaction(self, id_val, transaction):
self.slow_transactions[id_val] = transaction
self.check_matching()
def check_matching(self):
# Verify transactions cross domains correctly
for id_val in self.fast_transactions:
if id_val in self.slow_transactions:
fast_tx = self.fast_transactions[id_val]
slow_tx = self.slow_transactions[id_val]
# Compare transactions accounting for CDC latency
if not self.compare_cdc_transactions(fast_tx, slow_tx):
print(f"CDC mismatch for ID {id_val}")
tracker = CDCTracker()
# Connect tracker to monitors
fast_monitor.add_callback(tracker.on_fast_transaction)
slow_monitor.add_callback(tracker.on_slow_transaction)
# Run test with clock domain crossing
await Timer(10000, units='ns')
Best Practices¶
Monitor Configuration¶
- Connect both sides—one-sided verification only tells half the story
- Size
id_widthto the actual system, not the default - Set timeouts that reflect your interconnect's real latency
Performance Optimization¶
- Retire completed transactions in long tests
- Watch memory usage when transaction volume gets large
- Filter by ID when you're chasing something specific
Error Analysis¶
- Detailed logging is worth it for protocol violations
- Timestamps turn "it failed" into "it failed 40 ns after the request"
- Keep failed transaction pairs for debugging
Integration Guidelines¶
- Monitors connected before traffic starts, not after
- Scoreboard statistics double as coverage data
- Write custom callbacks for anything specialized
Integration Points¶
Test Environment Integration¶
# Integration with test sequence
class AXI4TestEnvironment:
def __init__(self, dut, clock):
self.scoreboard = AXI4Scoreboard("TestEnv", log=logger)
self.master_monitor = AXI4Monitor(dut.master, "Master", clock)
self.slave_monitor = AXI4Monitor(dut.slave, "Slave", clock)
self.scoreboard.add_master_monitor(self.master_monitor)
self.scoreboard.add_slave_monitor(self.slave_monitor)
def run_verification(self):
return self.scoreboard.report()
Coverage Integration¶
The ID dictionaries double as functional coverage:
# Functional coverage with scoreboard
def calculate_id_coverage():
used_ids = set(scoreboard.master_writes.keys()) | set(scoreboard.master_reads.keys())
total_ids = 2 ** scoreboard.id_width
coverage = len(used_ids) / total_ids * 100
print(f"ID Coverage: {coverage:.1f}% ({len(used_ids)}/{total_ids})")
Per-ID queues, dual-side pairing, and protocol checks running the whole time—this is the scoreboard you want between a master and a slave that don't finish transactions in the order they started them.