|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
AXISSlave¶
AXISSlave is the stream sink. It drives TREADY, captures beats as they handshake off the bus, and keeps count at the frame level — where a frame is the run of beats ending with TLAST. Like the master, it's a thin AXIS-flavored layer over the GAXI receive pipeline, which is where the real work happens.
Class Overview¶
class AXISSlave(GAXISlave):
"""
AXIS Slave component for receiving AXI4-Stream protocol.
Inherits from GAXISlave to reuse the structured pipeline state
machine, ready-signal driving, statistics, and base initialization
plumbing.
AXIS-specific features added by this subclass:
- Frame boundary detection via TLAST
- Packet and frame statistics
- apply_backpressure and wait_for_frame extensions
"""
Delegation to the GAXI Pipeline¶
The inherited GAXISlave receive pipeline performs all handshake detection, data capture, memory-model writes, and — importantly — all TREADY driving, including randomized ready delays taken from the slave randomizer. AXISSlave does not run a competing monitor loop or drive TREADY itself, so there is no contention over the ready signal. One driver, one owner.
AXIS-level frame tracking is layered on through the standard cocotb callback mechanism: every packet the GAXI pipeline captures is also passed to _axis_packet_callback, which maintains packets_received, frames_received, total_data_bytes, and the current-frame state used by get_current_frame_info() and wait_for_frame().
AXISSlave sets _default_packet_class = AXISPacket, so the packets the pipeline hands to that callback are real AXISPacket objects (see GAXIComponentBase._build_packet). An explicit packet_class= argument still takes precedence.
Note: here's the part that bites people. Registering a callback changes cocotb's delivery path —
Monitor._recv()only appends to_recvQwhen no callback is registered. So consumeAXISSlavetraffic through_axis_packet_callback, your ownadd_callback(), or the frame statistics — notslave._recvQ, which will sit there looking stubbornly empty.AXISMonitorsidesteps this by hooking_finish_packetinstead of adding a callback, which is why its_recvQstill works.
Constructor¶
__init__(dut, title, prefix, clock, **kwargs)¶
Construct the slave and resolve the bus signals sitting under prefix.
Parameters:
- dut - Device under test instance
- title (str) - Component title/name for identification and logging
- prefix (str) - Signal prefix (e.g., "s_axis_", "axis_")
- clock - Clock signal reference
Optional Parameters:
- field_config (AXISFieldConfigs) - Field configuration (creates default if None)
- timeout_cycles (int) - Maximum cycles for operations (default: 1000)
- mode (str) - Protocol mode ('skid', 'blocking', etc.)
- bus_name (str) - Bus/channel name for identification
- pkt_prefix (str) - Packet field prefix
- multi_sig (bool) - Whether using multi-signal mode
- randomizer - Optional randomizer for timing variations
- memory_model - Optional memory model integration
- log - Logger instance for debug output
- super_debug (bool) - Enable detailed debugging
- pipeline_debug (bool) - Enable pipeline debugging
- signal_map (dict) - Optional manual signal mapping
Example:
slave = AXISSlave(
dut=dut,
title="StreamSink",
prefix="s_axis_",
clock=clk,
timeout_cycles=2000,
super_debug=True
)
Core Methods¶
Flow Control and Ready Management¶
set_ready_always(ready=True)¶
Poke TREADY to a fixed value, right now.
Note: the GAXI receive pipeline actively manages TREADY around each handshake, so this override only holds between pipeline actions (for example while the pipeline is waiting for TVALID). For sustained or randomized backpressure, use
apply_backpressure()— which feeds the randomizer the pipeline actually consults — rather than pinning the signal here.
Parameters:
- ready (bool) - True to assert ready, False to deassert
Example:
# Nudge ready high right now
slave.set_ready_always(True)
# Nudge ready low right now
slave.set_ready_always(False)
apply_backpressure(probability=0.2, min_cycles=1, max_cycles=5)¶
This is how you do real backpressure. Instead of pinning TREADY yourself, you adjust the ready-delay constraints the receive pipeline consults — and the pipeline does the driving, so the backpressure survives across beats instead of being overwritten by the next pipeline action.
Parameters:
- probability (float) - Probability of applying backpressure (0.0 to 1.0)
- min_cycles (int) - Minimum cycles to hold ready low
- max_cycles (int) - Maximum cycles to hold ready low
Example:
# Apply 30% chance of 1-3 cycle backpressure
slave.apply_backpressure(
probability=0.3,
min_cycles=1,
max_cycles=3
)
Frame Monitoring and Reception¶
wait_for_frame(timeout_cycles=None) (async)¶
Block until a complete frame lands — that is, until a beat arrives with TLAST set.
Parameters:
- timeout_cycles (int) - Maximum cycles to wait (uses instance default if None)
Returns: bool - True if frame completed, False if timeout
Example:
# Wait for next frame with default timeout
frame_received = await slave.wait_for_frame()
# Wait with custom timeout
frame_received = await slave.wait_for_frame(timeout_cycles=5000)
if frame_received:
stats = slave.get_stats()
print(f"Frame received! Total frames: {stats['frames_received']}")
get_current_frame_info()¶
A snapshot of the frame currently in flight — useful for progress logging while a long frame is arriving.
Returns: dict - Frame information containing:
- packets_in_frame - Number of packets received in current frame
- frame_id - ID of the current frame (from first packet)
- total_bytes - Total bytes received in current frame
- is_receiving - Whether currently receiving a frame
Example:
frame_info = slave.get_current_frame_info()
print(f"Current frame: {frame_info['packets_in_frame']} packets, "
f"{frame_info['total_bytes']} bytes, ID={frame_info['frame_id']}")
Status and Statistics Methods¶
get_stats()¶
The counters, in one dict:
Returns: dict - Statistics dictionary containing:
- packets_received - Total packets received
- frames_received - Total frames received (sequences ending with TLAST)
- total_data_bytes - Total bytes received
- errors - Number of error events
- current_frame_info - Information about current frame being received
Example:
stats = slave.get_stats()
print(f"Received {stats['packets_received']} packets in {stats['frames_received']} frames")
print(f"Total data: {stats['total_data_bytes']} bytes")
print(f"Current frame: {stats['current_frame_info']['packets_in_frame']} packets")
Automatic Monitoring Features¶
Stream Reception Monitoring¶
Once constructed, the slave is already watching the bus. No explicit start, no polling loop in your test:
- Handshake detection: the pipeline watches TVALID/TREADY and acts on completed handshakes.
- Packet capture: valid transfers become
AXISPacketobjects automatically. - Frame assembly: beats are grouped by TLAST boundaries.
- Statistics tracking: counters update as packets land.
Frame Boundary Processing¶
- Automatic TLAST detection: frame boundaries are recognized, not polled for.
- Frame statistics: complete frames are counted as they close.
- Multi-frame support: back-to-back frames are tracked without you stitching anything together.
- Frame ID tracking: each frame is associated with the ID of its first beat.
Memory Model Integration¶
from CocoTBFramework.components.shared.memory_model import MemoryModel
# Attach a memory model at construction time
memory = MemoryModel(num_lines=256, bytes_per_line=4)
slave = AXISSlave(dut, "Sink", "s_axis_", clk, memory_model=memory)
# All received data is automatically written to memory
# Memory addresses are calculated based on packet fields
Advanced Features¶
Protocol Compliance Monitoring¶
Along the way, the component also keeps an eye on protocol health:
- Validates TVALID/TREADY handshake timing
- Checks protocol specification compliance
- Detects and reports protocol violations
- Monitors sideband signal consistency
Timing Randomization¶
# Create randomizer for realistic ready timing
from CocoTBFramework.components.shared.flex_randomizer import FlexRandomizer
randomizer = FlexRandomizer({
'ready_delay': ([(0, 0), (1, 5)], [0.8, 0.2])
})
slave = AXISSlave(
dut=dut,
title="RealisticSink",
prefix="s_axis_",
clock=clk,
randomizer=randomizer
)
# Randomizer automatically applies ready timing variations
Debug and Analysis¶
# Enable comprehensive debugging
slave = AXISSlave(
dut=dut,
title="DebugSlave",
prefix="s_axis_",
clock=clk,
super_debug=True,
pipeline_debug=True
)
# All received packets are logged with detailed information
Integration Examples¶
Basic Stream Reception¶
async def test_stream_reception():
# Create slave
slave = AXISSlave(dut, "Receiver", "s_axis_", clk)
# Set to always ready
slave.set_ready_always(True)
# Wait for data
await slave.wait_for_frame(timeout_cycles=1000)
# Check results
stats = slave.get_stats()
assert stats['frames_received'] > 0, "No frames received"
print(f"Successfully received {stats['packets_received']} packets")
Backpressure Testing¶
async def test_backpressure():
slave = AXISSlave(dut, "BackpressureSink", "s_axis_", clk)
# Apply moderate backpressure
slave.apply_backpressure(
probability=0.4, # 40% chance
min_cycles=1,
max_cycles=4
)
# Monitor reception with backpressure
initial_time = get_sim_time()
await slave.wait_for_frame(timeout_cycles=5000)
reception_time = get_sim_time() - initial_time
print(f"Reception time with backpressure: {reception_time}")
Multi-Stream Monitoring¶
async def test_multi_stream():
slave = AXISSlave(dut, "MultiStreamSink", "s_axis_", clk)
slave.set_ready_always(True)
# Monitor multiple frames
frame_count = 0
target_frames = 4
while frame_count < target_frames:
if await slave.wait_for_frame(timeout_cycles=2000):
frame_count += 1
frame_info = slave.get_current_frame_info()
print(f"Frame {frame_count}: ID={frame_info['frame_id']}")
else:
break
stats = slave.get_stats()
assert stats['frames_received'] == target_frames
Memory Verification¶
async def test_memory_verification():
# Create memory model
from CocoTBFramework.components.shared.memory_model import MemoryModel
memory = MemoryModel(num_lines=512, bytes_per_line=4)
# Create slave with memory integration
slave = AXISSlave(
dut, "MemorySink", "s_axis_", clk,
memory_model=memory
)
# Receive data (automatically written to memory)
await slave.wait_for_frame()
# Verify memory contents
received_data = memory.read(0x0, 64) # Read first 64 bytes
expected_data = generate_expected_pattern()
assert received_data == expected_data, "Memory verification failed"
Error Handling and Recovery¶
Failures surface the way they should — as timeouts you can catch and exceptions that get logged, not as silently missing data:
- Timeout detection: configurable timeout for operations
- Exception recovery: graceful handling of simulation exceptions
- Protocol error detection: identification of protocol violations
- Logging integration: detailed error reporting through the component logger
Common error scenarios: - TVALID timeout — no data ever arrived (dead master, wrong prefix, or a DUT that never started) - Protocol violations — an invalid handshake sequence - Memory model write failures - Clock domain issues
Performance Monitoring¶
Reception Statistics¶
The counters give you the raw material for throughput analysis:
- Throughput: packets and bytes received — divide by simulation time yourself, as in the example below
- Frame statistics: frame completion counts and sizes
- Backpressure impact: ready signal timing behavior
- Protocol efficiency: handshake success rates
Real-time Analysis¶
# Monitor real-time performance
stats = slave.get_stats()
frame_info = slave.get_current_frame_info()
print(f"Throughput: {stats['total_data_bytes'] / simulation_time} MB/s")
print(f"Frame completion rate: {stats['frames_received'] / elapsed_frames}")
print(f"Current frame progress: {frame_info['packets_in_frame']} packets")
Integration with Other Components¶
Master-Slave Pairs¶
# Create matched master-slave pair
master = AXISMaster(dut, "Source", "m_axis_", clk)
slave = AXISSlave(dut, "Sink", "s_axis_", clk)
# Configure matching parameters
config = AXISFieldConfigs.create_default_axis_config()
master.field_config = config
slave.field_config = config
Monitor Integration¶
# Add monitor for comprehensive analysis
monitor = AXISMonitor(dut, "StreamMonitor", "s_axis_", clk)
slave = AXISSlave(dut, "StreamSink", "s_axis_", clk)
# Both components monitor the same signals
# Monitor provides protocol analysis
# Slave provides reception functionality
For most tests the pattern is short: construct the slave, maybe call apply_backpressure(...), await wait_for_frame(), then check get_stats(). Everything else on this page is there for the day that isn't enough.