3. Python-UVM Integration - Test Patterns & Best Practices

In the previous post, we built the DPI-C bridge connecting SystemVerilog to Python. Now, let's look at how to write actual tests using this bridge. One of the biggest advantages of this approach is being able to use Python's clean syntax and powerful standard library for verification.

The Factory Pattern

To keep the C-to-Python bridge simple, we use a single entry point for all tests: the create_sequence() factory function. This allows the C layer to load any test module without knowing its internal details.

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#dbeafe', 'primaryTextColor': '#1e293b', 'primaryBorderColor': '#3b82f6', 'lineColor': '#64748b', 'secondaryColor': '#f1f5f9'}}}%%
flowchart LR
    subgraph PY["Python Layer"]
        FACT([create_sequence])
        SEQ[APBSequence]
        STIM[Stimulus]
    end

    subgraph BRIDGE["DPI-C Bridge"]
        C_INIT[apb_init]
        C_EXEC[dpi_get_transaction]
    end
    
    subgraph SV["SystemVerilog"]
        DUT{{DUT}}
    end

    C_INIT -->|Calls| FACT
    FACT -->|Returns| SEQ
    SEQ o--o STIM
    C_EXEC -->|Polls| SEQ
    SEQ -->|Txn| C_EXEC
    C_EXEC ==> DUT

    style PY fill:#d1fae5,stroke:#10b981
    style BRIDGE fill:#f1f5f9,stroke:#64748b
    style SV fill:#dbeafe,stroke:#3b82f6
# apb_basic_test.py
from apb_base import APBSequence

def create_sequence():
    """Factory function called by DPI bridge"""
    seq = APBSequence("Basic_Test")
    
    # Test stimulus defined here
    seq.add_write(0x10, 0x12345678)
    seq.add_read(0x10)
    
    return seq

Fluent Pattern for Stimulus

Instead of the verbose UVM macros (`uvm_do_with), we can use Python's method chaining (Fluent Interface) to make sequences readable and concise. The APBSequence class in apb_base.py returns self from its methods to enable this.

# apb_base.py snippet
class APBSequence:
    def add_write(self, addr, data):
        # ... create txn ...
        return self  # Enables chaining

    def add_read(self, addr):
        # ... create txn ...
        return self

This allows us to write compact directed tests:

# Directed test sequence
seq.add_write(0x10, 0xAA) \
   .add_write(0x14, 0xBB) \
   .add_read(0x10) \
   .add_read(0x14)

Constrained Random in Python

SystemVerilog's constraint solver is powerful, but for many block-level verification tasks, Python's random module is more than sufficient and significantly faster to write/debug.

# apb_random_test.py
from apb_base import APBRandomSequence

def create_sequence():
    # Generate 20 random transactions
    # No complex constraints, just Python logic
    return APBRandomSequence(
        num_transactions=20,
        addr_range=(0x0, 0x1FF)
    )

The implementation of APBRandomSequence shows how easy it is to implement custom randomization logic:

class APBRandomSequence(APBSequence):
    def __init__(self, num_transactions, addr_range, name="Random"):
        super().__init__(name)
        import random
        
        for _ in range(num_transactions):
            # Align address to 4 bytes
            addr = random.randint(*addr_range) & 0xFFFFFFFC
            data = random.randint(0, 0xFFFFFFFF)
            is_write = random.choice([True, False])
            
            if is_write:
                self.add_write(addr, data)
            else:
                self.add_read(addr)

Verification Pattern: Callbacks

In a standard UVM sequence, getting read data back can be tricky if you're mixing writes and reads. In our Python structure, we can register callbacks for read data. This separates the stimulus generation from the checking logic.

def check_status(data):
    if (data & 0x1) == 0:
        print("[ERROR] Status bit 0 not set!")

# Register callback with read
seq.add_write(0x00, 0x1) \
   .add_read(0x00, callback=check_status)

The base class handles invoking the callback when data returns from the C bridge:

# apb_base.py
def send_read_data(self, sim_time, data):
    # ... log data ...
    if callback:
        callback(data)

Key Takeaways

  • Unified Entry Point: The factory pattern decouples the C bridge from specific tests.
  • Readable Tests: Method chaining creates cleaner code than deeply nested UVM macros.
  • Simple Randomization: Standard Python libraries often replace complex constraint solvers for driver-level randomness.
  • Functional Checking: Passing functions as callbacks is a natural pattern in Python (first-class functions).

In the next post, we will look at the APB VIP Architecture and how to package this into a reusable Verification IP.

Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment