1. Python-UVM Integration - Introduction & Overview

What if you could write UVM tests in Python? Leverage Python's rich ecosystem, rapid prototyping, and intuitive syntax while still running full UVM simulations? This post introduces a Python-to-SystemVerilog DPI-C bridge that makes this possible.

The Problem with Traditional UVM Development

Traditional UVM test development follows a slow iteration cycle:

flowchart LR
    A["Edit SV Code"] --> B["Compile"]
    B --> C["Elaborate"]
    C --> D["Simulate"]
    D --> E["Debug"]
    E --> A
    
    style B fill:#fee2e2,stroke:#ef4444
    style C fill:#fee2e2,stroke:#ef4444

Pain points:

  • Long compile times - Even small test changes require full recompilation
  • Verbose syntax - SystemVerilog constraints and sequences are powerful but verbose
  • Limited libraries - No easy access to Python's numpy, pandas, ML frameworks
  • Debug iteration - Print-based debugging requires recompilation
Key Insight: What if test stimulus could be defined in Python while the UVM infrastructure handles protocol driving and checking?

The Solution: Python-C-SystemVerilog Bridge

The solution is a three-layer architecture that connects Python scripts to UVM sequences via DPI-C:

flowchart TB
    subgraph Python["Python Layer"]
        P1["Test Script\n(apb_basic_test.py)"]
        P2["APBDriver Class"]
        P3["Transaction Queue"]
    end
    
    subgraph C["C Bridge Layer"]
        C1["Python C API\n(Py_Initialize, PyObject_Call)"]
        C2["DPI-C Functions\n(dpi_get_transaction)"]
    end
    
    subgraph SV["SystemVerilog Layer"]
        S1["DPI Imports"]
        S2["UVM Sequence"]
        S3["APB Agent"]
        S4["DUT"]
    end
    
    P1 --> P2
    P2 --> P3
    P3 <-.->|"Transactions"| C1
    C1 <--> C2
    C2 <-.->|"DPI Calls"| S1
    S1 --> S2
    S2 --> S3
    S3 -->|"APB Protocol"| S4
    
    style Python fill:#dbeafe,stroke:#3b82f6
    style C fill:#fef3c7,stroke:#f59e0b
    style SV fill:#d1fae5,stroke:#10b981

How It Works

  1. Python test creates transactions using a fluent API
  2. C bridge manages Python interpreter and exposes DPI functions
  3. UVM sequence calls DPI to fetch transactions from Python
  4. APB agent drives transactions to DUT using standard UVM
  5. Read data flows back to Python for verification

The Python API: Fluent and Intuitive

Tests are written using a clean, chainable API:

# apb_basic_test.py
from apb_driver import APBDriver

def run_test():
    driver = APBDriver()
    
    # Fluent API - chain multiple operations
    driver.write(0x00, 0x12345678) \
          .write(0x04, 0xDEADBEEF) \
          .read(0x00) \
          .read(0x04) \
          .write(0x08, 0xCAFEBABE) \
          .read(0x08)
    
    return driver.get_transactions()

Compare this to equivalent SystemVerilog sequence code:

// Traditional UVM sequence - more verbose
class apb_basic_seq extends uvm_sequence #(apb_seq_item);

  `uvm_object_utils(apb_basic_seq)

  task body();
    apb_seq_item req;

    // Write 0x12345678 to address 0x00
    req = apb_seq_item::type_id::create("req");
    start_item(req);
    req.addr  = 32'h00;
    req.data  = 32'h12345678;
    req.write = 1;
    finish_item(req);

    // Write 0xDEADBEEF to address 0x04
    req = apb_seq_item::type_id::create("req");
    start_item(req);
    req.addr  = 32'h04;
    req.data  = 32'hDEADBEEF;
    req.write = 1;
    finish_item(req);

    // ... and so on for each transaction
  endtask

endclass

Key Benefits

BenefitDescription
Rapid IterationChange Python test, re-run simulation - no recompilation needed
Python EcosystemUse numpy for data generation, pandas for analysis, pytest for organization
Readable TestsFluent API makes test intent clear at a glance
Data-Driven TestingLoad test vectors from CSV, JSON, or databases
ML IntegrationGenerate stimulus using machine learning models
Standard UVMFull UVM infrastructure for protocol handling, coverage, checking

Architecture Deep Dive

Layer 1: Python Infrastructure

# Base transaction class
class APBTransaction:
    def __init__(self, addr, data, is_write):
        self.addr = addr
        self.data = data
        self.is_write = is_write

# Driver with fluent API
class APBDriver:
    def __init__(self):
        self.transactions = []
    
    def write(self, addr, data):
        self.transactions.append(APBTransaction(addr, data, True))
        return self  # Enable chaining
    
    def read(self, addr):
        self.transactions.append(APBTransaction(addr, 0, False))
        return self  # Enable chaining
    
    def get_transactions(self):
        return self.transactions

Layer 2: C Bridge (DPI-C)

// Core DPI functions exposed to SystemVerilog
#include "Python.h"
#include "svdpi.h"

static PyObject* py_module = NULL;
static PyObject* py_transactions = NULL;

// Initialize Python and load test module
int dpi_init_python(const char* module_name) {
    Py_Initialize();
    py_module = PyImport_ImportModule(module_name);
    
    // Call run_test() to get transactions
    PyObject* func = PyObject_GetAttrString(py_module, "run_test");
    py_transactions = PyObject_CallObject(func, NULL);
    
    return (py_transactions != NULL);
}

// Get next transaction from Python queue
int dpi_get_transaction(uint32_t* addr, uint32_t* data, uint8_t* is_write) {
    // Fetch from Python list, populate output arguments
    // Return 0 when queue empty
}

Layer 3: SystemVerilog Sequence

class apb_python_seq extends uvm_sequence #(apb_seq_item);

  `uvm_object_utils(apb_python_seq)

  // DPI imports
  import "DPI-C" function int dpi_init_python(string module_name);
  import "DPI-C" function int dpi_get_transaction(
    output int addr, output int data, output byte is_write
  );
  import "DPI-C" function void dpi_send_read_data(int data);

  task body();
    apb_seq_item req;
    int addr, data;
    byte is_write;
    string test_name;

    // Get test name from environment variable
    if (!$value$plusargs("APB_TEST=%s", test_name)) begin
      test_name = "apb_basic_test";
    end

    // Initialize Python and load test
    if (!dpi_init_python(test_name)) begin
      `uvm_fatal("DPI", "Failed to initialize Python")
    end

    // Fetch and drive transactions from Python
    while (dpi_get_transaction(addr, data, is_write)) begin
      req = apb_seq_item::type_id::create("req");
      start_item(req);
      req.addr  = addr;
      req.data  = data;
      req.write = is_write;
      finish_item(req);

      // Send read data back to Python
      if (!is_write) begin
        dpi_send_read_data(req.data);
      end
    end
  endtask

endclass

Running a Simulation

# Build the DPI bridge library
gcc -shared -fPIC -o dpi_bridge.so \
    dpi_bridge.c \
    $(python3-config --includes) \
    $(python3-config --ldflags)

# Run simulation with Python test
xsim top -sv_lib dpi_bridge +APB_TEST=apb_basic_test

Available Test Patterns

TestDescriptionTransactions
apb_basic_testSequential read/write operations6
apb_burst_testConsecutive burst operations16
apb_random_testRandomized address/data patterns20

Use Cases

1. Data-Driven Testing

import csv

def run_test():
    driver = APBDriver()
    
    # Load test vectors from CSV
    with open('test_vectors.csv') as f:
        for row in csv.DictReader(f):
            if row['type'] == 'write':
                driver.write(int(row['addr'], 16), int(row['data'], 16))
            else:
                driver.read(int(row['addr'], 16))
    
    return driver.get_transactions()

2. Algorithmic Stimulus Generation

import numpy as np

def run_test():
    driver = APBDriver()
    
    # Generate walking ones pattern
    for i in range(32):
        driver.write(0x100, 1 << i).read(0x100)
    
    # Random stress test using numpy
    addrs = np.random.randint(0, 0x1000, size=100) & ~0x3  # Aligned
    for addr in addrs:
        driver.write(int(addr), np.random.randint(0, 0xFFFFFFFF))
    
    return driver.get_transactions()

3. Register Map Testing

def run_test():
    driver = APBDriver()
    
    # Define register map
    registers = {
        'CTRL':   0x00,
        'STATUS': 0x04,
        'DATA':   0x08,
        'CONFIG': 0x0C,
    }
    
    # Initialize all registers
    driver.write(registers['CTRL'], 0x00000001)   # Enable
    driver.write(registers['CONFIG'], 0x00000F0F) # Configure
    
    # Verify initialization
    for name, addr in registers.items():
        driver.read(addr)
    
    return driver.get_transactions()

Project Structure

apb2/
├── apb_requester/     # APB master agent (UVM)
├── apb_completer/     # APB slave agent (UVM)
├── apb_env/           # UVM environment, transactions
├── apb_test/          # UVM test classes
├── reset_agent/       # Reset control agent
├── top/               # Top-level testbench
└── sim/
    ├── dpi_bridge/    # C bridge source code
    │   ├── dpi_core.c       # Python lifecycle management
    │   └── dpi_apb_plugin.c # APB-specific DPI functions
    └── tests/         # Python test scripts
        ├── apb_driver.py    # APBDriver class
        ├── apb_basic_test.py
        ├── apb_burst_test.py
        └── apb_random_test.py

What's Next

This post introduced the concept and architecture. Future posts will cover:

  • DPI-C Deep Dive - Building the C bridge, Python C API, memory management
  • Python Test Patterns - Advanced testing techniques, debugging, coverage
  • APB VIP Architecture - Complete UVM agent implementation

Check out the full implementation: github.com/mayurkubavat/SV-Python

Interview Questions

Q1: What is DPI-C in SystemVerilog?

Answer: DPI-C (Direct Programming Interface for C) allows SystemVerilog to call C functions and vice versa. Functions are declared with import "DPI-C" for calling C from SV, or export "DPI-C" for calling SV from C. It enables integration with external libraries, Python interpreters, and system-level code.

Q2: Why would you use Python for UVM test stimulus?

Answer: Python offers rapid iteration (no recompilation), access to rich libraries (numpy, pandas, ML frameworks), readable syntax, and easy data-driven testing from files. The UVM infrastructure still handles protocol driving, checking, and coverage while Python focuses on stimulus generation.

Q3: How does bidirectional communication work in a DPI bridge?

Answer: The C bridge maintains Python objects across DPI calls. SystemVerilog calls dpi_get_transaction() to fetch stimulus from Python, and dpi_send_read_data() to pass read results back. The C layer uses Python C API functions like PyObject_CallMethod() to interact with Python objects.

Key Takeaways

  • DPI-C bridge connects Python to SystemVerilog via C layer
  • Python tests use fluent API for readable, chainable operations
  • No recompilation needed when changing Python test scripts
  • Full UVM infrastructure handles protocol, coverage, checking
  • Bidirectional communication enables read data verification in Python
  • Python ecosystem available for data generation, ML, file I/O
Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment