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
- Python test creates transactions using a fluent API
- C bridge manages Python interpreter and exposes DPI functions
- UVM sequence calls DPI to fetch transactions from Python
- APB agent drives transactions to DUT using standard UVM
- 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
| Benefit | Description |
|---|---|
| Rapid Iteration | Change Python test, re-run simulation - no recompilation needed |
| Python Ecosystem | Use numpy for data generation, pandas for analysis, pytest for organization |
| Readable Tests | Fluent API makes test intent clear at a glance |
| Data-Driven Testing | Load test vectors from CSV, JSON, or databases |
| ML Integration | Generate stimulus using machine learning models |
| Standard UVM | Full 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
| Test | Description | Transactions |
|---|---|---|
apb_basic_test | Sequential read/write operations | 6 |
apb_burst_test | Consecutive burst operations | 16 |
apb_random_test | Randomized address/data patterns | 20 |
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
Comments (0)
Leave a Comment