CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-cocotb

Cocotb is a coroutine-based cosimulation library for writing VHDL and Verilog testbenches in Python.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

signal-handling.mddocs/

Signal Handling

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.

Capabilities

Base Handle Classes

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}")

Hierarchical Objects

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}")

Value Objects

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}")

Signal Actions

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")

Handle Factory

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__}")

Advanced Signal Handling

Dynamic Signal Discovery

@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)}")

Signal Monitoring

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}")

Complex Signal Operations

@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:
                break

Install with Tessl CLI

npx tessl i tessl/pypi-cocotb

docs

binary-types.md

clock-generation.md

index.md

logging-utilities.md

signal-handling.md

task-management.md

test-framework.md

triggers-timing.md

tile.json