Cocotb is a coroutine-based cosimulation library for writing VHDL and Verilog testbenches in Python.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Cocotb's signal handling system provides hierarchical access to HDL design objects through a comprehensive handle system. It supports reading, writing, forcing, and releasing signals across different HDL types with automatic type conversion and simulator integration.
Foundation classes for all HDL object access with common properties and methods.
class SimHandleBase:
"""
Base class for all simulation object handles.
Properties:
- _name: Signal name
- _type: HDL type information
- _fullname: Full hierarchical name
- _path: Hierarchical path
- _def_name: Definition name
- _def_file: Definition file
"""
def get_definition_name(self):
"""
Get the definition name of the object.
Returns:
Definition name string or empty string if not available
"""
def get_definition_file(self):
"""
Get the definition file of the object.
Returns:
Definition file path or empty string if not available
"""Usage Examples:
@cocotb.test()
async def handle_info_test(dut):
"""Test demonstrating handle information access."""
# Access handle properties
cocotb.log.info(f"DUT name: {dut._name}")
cocotb.log.info(f"DUT type: {dut._type}")
cocotb.log.info(f"DUT full name: {dut._fullname}")
# Get definition information
def_name = dut.get_definition_name()
def_file = dut.get_definition_file()
cocotb.log.info(f"Defined as: {def_name} in {def_file}")
# Access sub-signals
if hasattr(dut, 'cpu'):
cpu_def = dut.cpu.get_definition_name()
cocotb.log.info(f"CPU module: {cpu_def}")Objects that contain child elements and support attribute-based access to design hierarchy.
class HierarchyObject(SimHandleBase):
"""
Hierarchical objects with child attribute access.
Supports:
- Attribute access to child objects (dut.cpu.registers.pc)
- Dynamic discovery of child objects
- Hierarchical navigation
"""
class HierarchyArrayObject(HierarchyObject):
"""
Arrays of hierarchical objects.
Supports:
- Indexed access to array elements (dut.memory[0])
- Slicing operations (dut.memory[0:7])
- Iteration over elements
"""
class RegionObject(HierarchyObject):
"""
Region/namespace objects in design hierarchy.
Represents logical groupings like packages, scopes,
and generate blocks.
"""Usage Examples:
@cocotb.test()
async def hierarchy_test(dut):
"""Test demonstrating hierarchical object access."""
# Navigate design hierarchy
cpu = dut.cpu_subsystem.cpu_core
registers = cpu.register_file
# Access specific register
program_counter = registers.pc
cocotb.log.info(f"PC value: {program_counter.value}")
# Access array elements
memory = dut.memory_controller.ram
# Access specific memory location
mem_location = memory[0x100]
cocotb.log.info(f"Memory[256]: {mem_location.value}")
# Iterate over array elements (if supported)
for i in range(8):
register = registers._id(f"r{i}", extended=False)
cocotb.log.info(f"R{i}: {register.value}")
# Access generate block instances
for i in range(4):
instance = dut._id(f"gen_block[{i}].instance", extended=False)
cocotb.log.info(f"Generated instance {i}: {instance._name}")Objects representing actual signal values with read/write capabilities.
class NonHierarchyObject(SimHandleBase):
"""Base class for non-hierarchical (leaf) objects."""
class ConstantObject(NonHierarchyObject):
"""
Read-only constant objects.
Properties:
- value: Current value (read-only)
"""
class NonConstantObject(NonHierarchyObject):
"""
Non-constant signal objects with read capability.
Properties:
- value: Current signal value
"""
class ModifiableObject(NonConstantObject):
"""
Modifiable signal objects with read/write capability.
Properties:
- value: Current signal value (read/write)
"""
class IntegerObject(ModifiableObject):
"""
Integer signal objects.
Optimized for integer values with automatic conversion.
"""
class RealObject(ModifiableObject):
"""
Real number signal objects.
Supports floating-point signal values.
"""
class EnumObject(ModifiableObject):
"""
Enumeration signal objects.
Supports enumerated type values.
"""
class StringObject(ModifiableObject):
"""
String variable objects.
Supports string values in HDL variables.
"""
class NonHierarchyIndexableObject(NonConstantObject):
"""
Indexable arrays and memories.
Supports:
- Indexed access (signal[index])
- Slicing operations (signal[start:end])
- Bit manipulation
"""Usage Examples:
@cocotb.test()
async def value_objects_test(dut):
"""Test demonstrating value object operations."""
# Read constant values
version_reg = dut.version_register # ConstantObject
cocotb.log.info(f"Version: {version_reg.value}")
# Read/write integer signals
counter = dut.counter # IntegerObject
original_value = counter.value
counter.value = 42
assert counter.value == 42
# Real number signals
if hasattr(dut, 'voltage_level'):
voltage = dut.voltage_level # RealObject
voltage.value = 3.3
cocotb.log.info(f"Voltage set to: {voltage.value}")
# Enumeration signals
if hasattr(dut, 'state'):
state = dut.state # EnumObject
state.value = "IDLE"
cocotb.log.info(f"State: {state.value}")
# String variables
if hasattr(dut, 'debug_message'):
message = dut.debug_message # StringObject
message.value = "Test message"
# Indexable objects (arrays, bit vectors)
data_bus = dut.data_bus # NonHierarchyIndexableObject
# Access individual bits
lsb = data_bus[0]
msb = data_bus[31]
# Access bit slices
lower_byte = data_bus[7:0]
upper_word = data_bus[31:16]
# Modify bit slices
lower_byte.value = 0xFF
upper_word.value = 0xABCD
cocotb.log.info(f"Data bus: {data_bus.value}")Control signal behavior beyond simple value assignment.
class Deposit(value):
"""
Deposit value action (simulator-dependent behavior).
Parameters:
- value: Value to deposit
Usage:
signal.value = Deposit(42)
"""
class Force(value):
"""
Force signal to specific value.
Overrides normal signal driving until released.
Parameters:
- value: Value to force
Usage:
signal.value = Force(1)
"""
class Freeze():
"""
Freeze signal at current value.
Prevents signal changes until released.
Usage:
signal.value = Freeze()
"""
class Release():
"""
Release forced or frozen signal.
Returns signal to normal operation.
Usage:
signal.value = Release()
"""Usage Examples:
@cocotb.test()
async def signal_actions_test(dut):
"""Test demonstrating signal actions."""
# Normal assignment
dut.control_signal.value = 1
await Timer(100, units="ns")
# Force signal to specific value
dut.control_signal.value = Force(0)
cocotb.log.info("Control signal forced to 0")
# Signal will remain 0 even if logic tries to change it
await Timer(200, units="ns")
assert dut.control_signal.value == 0
# Freeze at current value
dut.status_signal.value = Freeze()
original_status = dut.status_signal.value
await Timer(100, units="ns")
assert dut.status_signal.value == original_status
# Release forced/frozen signals
dut.control_signal.value = Release()
dut.status_signal.value = Release()
cocotb.log.info("Signals released to normal operation")
# Deposit value (simulator-specific behavior)
dut.memory_data.value = Deposit(0xDEADBEEF)
await Timer(50, units="ns")
@cocotb.test()
async def debug_force_test(dut):
"""Test using force for debugging."""
# Force clock for debugging
dut.clk.value = Force(0)
await Timer(50, units="ns")
dut.clk.value = Force(1)
await Timer(50, units="ns")
# Release and return to normal clock
dut.clk.value = Release()
# Continue with normal test
await Timer(100, units="ns")Create appropriate handle objects for simulator objects.
def SimHandle(handle, path=None):
"""
Factory function to create appropriate handle object.
Automatically selects correct handle type based on
simulator object characteristics.
Parameters:
- handle: Simulator handle object
- path: Optional hierarchical path
Returns:
Appropriate SimHandleBase subclass instance
"""Usage Examples:
@cocotb.test()
async def handle_factory_test(dut):
"""Test demonstrating handle factory usage."""
# Factory automatically selects appropriate handle type
from cocotb import simulator
# Get raw simulator handle
raw_handle = simulator.get_handle_by_name("dut.cpu.pc")
# Convert to appropriate cocotb handle
pc_handle = SimHandle(raw_handle, "dut.cpu.pc")
# Use the handle normally
cocotb.log.info(f"PC handle type: {type(pc_handle)}")
cocotb.log.info(f"PC value: {pc_handle.value}")
# Factory handles different signal types automatically
signals_to_test = [
"data_bus", # Likely NonHierarchyIndexableObject
"counter", # Likely IntegerObject
"enable", # Likely ModifiableObject
"cpu", # Likely HierarchyObject
"memory" # Likely HierarchyArrayObject
]
for signal_name in signals_to_test:
if hasattr(dut, signal_name):
signal = getattr(dut, signal_name)
cocotb.log.info(f"{signal_name}: {type(signal).__name__}")@cocotb.test()
async def discovery_test(dut):
"""Test demonstrating dynamic signal discovery."""
# Discover all signals in a module
def discover_signals(module, prefix=""):
signals = []
# Try common signal name patterns
test_names = [
"clk", "clock", "reset", "rst", "enable", "valid", "ready",
"data", "addr", "address", "control", "status"
]
for name in test_names:
try:
signal = module._id(name, extended=False)
signals.append(f"{prefix}.{name}")
cocotb.log.info(f"Found signal: {prefix}.{name}")
except:
pass # Signal doesn't exist
return signals
# Discover signals in DUT
dut_signals = discover_signals(dut, "dut")
# Try to discover sub-modules
sub_modules = ["cpu", "memory", "controller", "interface"]
for sub_name in sub_modules:
try:
sub_module = dut._id(sub_name, extended=False)
sub_signals = discover_signals(sub_module, f"dut.{sub_name}")
dut_signals.extend(sub_signals)
except:
pass # Sub-module doesn't exist
cocotb.log.info(f"Total signals discovered: {len(dut_signals)}")class SignalMonitor:
"""Generic signal monitor for value tracking."""
def __init__(self, signal, name=None):
self.signal = signal
self.name = name or str(signal._name)
self.history = []
self.monitor_task = None
async def start_monitoring(self):
"""Start monitoring signal changes."""
self.monitor_task = cocotb.start_soon(self._monitor_loop())
def stop_monitoring(self):
"""Stop monitoring signal changes."""
if self.monitor_task:
self.monitor_task.kill()
async def _monitor_loop(self):
"""Internal monitoring loop."""
last_value = self.signal.value
self.history.append((get_sim_time("ns"), last_value))
while True:
await Edge(self.signal)
current_value = self.signal.value
current_time = get_sim_time("ns")
self.history.append((current_time, current_value))
cocotb.log.info(f"{self.name}: {last_value} -> {current_value} @ {current_time}ns")
last_value = current_value
def get_history(self):
"""Get signal change history."""
return self.history.copy()
@cocotb.test()
async def monitoring_test(dut):
"""Test demonstrating signal monitoring."""
# Create monitors
clk_monitor = SignalMonitor(dut.clk, "Clock")
data_monitor = SignalMonitor(dut.data_bus, "Data Bus")
# Start monitoring
await clk_monitor.start_monitoring()
await data_monitor.start_monitoring()
# Generate activity
for i in range(10):
dut.data_bus.value = i
await Timer(50, units="ns")
# Stop monitoring
clk_monitor.stop_monitoring()
data_monitor.stop_monitoring()
# Analyze history
data_history = data_monitor.get_history()
cocotb.log.info(f"Data bus changed {len(data_history)} times")
for timestamp, value in data_history[-5:]: # Last 5 changes
cocotb.log.info(f" {timestamp}ns: {value}")@cocotb.test()
async def complex_operations_test(dut):
"""Test demonstrating complex signal operations."""
# Multi-bit manipulation
control_reg = dut.control_register # 32-bit register
# Set individual control bits
control_reg[0].value = 1 # Enable bit
control_reg[1].value = 0 # Reset bit
control_reg[7:4].value = 0xA # Mode bits
# Read back constructed value
final_value = control_reg.value
cocotb.log.info(f"Control register: 0x{final_value:08x}")
# Array operations
if hasattr(dut, 'register_file'):
reg_file = dut.register_file
# Initialize register file
for i in range(32):
try:
reg = reg_file[i]
reg.value = i * 0x10 # Pattern
except:
break # Fewer registers than expected
# Verify pattern
for i in range(8): # Check first 8
try:
reg = reg_file[i]
expected = i * 0x10
assert reg.value == expected, f"R{i}: expected {expected}, got {reg.value}"
except:
break
# Memory-mapped operations
if hasattr(dut, 'memory'):
memory = dut.memory
# Write test pattern
test_data = [0xDEAD, 0xBEEF, 0xCAFE, 0xBABE]
for i, data in enumerate(test_data):
try:
memory[i].value = data
except:
pass # Memory might not support direct indexing
# Read back and verify
for i, expected in enumerate(test_data):
try:
actual = memory[i].value
assert actual == expected, f"Memory[{i}] mismatch"
except:
breakInstall with Tessl CLI
npx tessl i tessl/pypi-cocotb