4. Python-UVM Integration - Architecture at Scale
In the previous posts, we built a DPI-C bridge and explored Python test patterns for block-level verification. But the real power of Python in verification shines when we scale up to full System-on-Chip (SoC) designs and complex algorithmic blocks.
This post transitions from "how to build the bridge" to "how to architect the system." We'll explore three powerful architectural patterns:
- Algorithmic Reference Modeling - The "Sidecar" pattern
- Firmware Co-Simulation - The "Abstract Firmware" pattern
- System-Level Orchestration - The "Hub" pattern
1. Algorithmic Reference Modeling (The "Sidecar")
For blocks like DSP filters, AI/ML accelerators, or Cryptography engines, the verification challenge isn't just "does usage match the spec?" but "is the math correct?"
Writing bit-exact floating-point reference models in SystemVerilog is painful. In Python, it's trivial thanks to numpy and scipy.
The Architecture
In the Sidecar model, Python runs alongside the UVM environment. It monitors inputs, computes the Golden Reference output using standard libraries, and compares it against the RTL output.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e0f2fe', 'primaryTextColor': '#0f172a', 'primaryBorderColor': '#0066cc', 'lineColor': '#475569', 'secondaryColor': '#f8fafc', 'tertiaryColor': '#ffffff'}}}%%
flowchart LR
subgraph SV["SystemVerilog / UVM"]
direction TB
DRV[Driver]
MON_IN[Input\nMonitor]
MON_OUT[Output\nMonitor]
DUT{{FFT Engine}}
DRV ==>|stimulus| DUT
DUT ==> MON_OUT
DRV -.-> MON_IN
end
subgraph PY["Python Sidecar"]
direction TB
REF["numpy.fft\nReference"]
CHECK["Scoreboard"]
REF -->|expected| CHECK
end
MON_IN -.->|DPI: input data| REF
MON_OUT -.->|DPI: RTL result| CHECK
style DUT fill:#d1fae5,stroke:#10b981,stroke-width:2px
style REF fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
style CHECK fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
The key insight: Python becomes a verification co-processor, handling complex math while UVM handles protocol and timing.
Implementation Example
import numpy as np
def check_fft_packet(data_in, rtl_real, rtl_imag):
"""Verify FFT hardware output against numpy"""
# Create complex input vector
complex_in = [complex(r, i) for r, i in data_in]
# Golden Reference Calculation
expected = np.fft.fft(complex_in)
# Compare with tolerance (hardware precision is limited)
actual = [complex(r, i) for r, i in zip(rtl_real, rtl_imag)]
if not np.allclose(expected, actual, atol=1e-5):
print(f"[MISMATCH] Expected {expected[0]}... Got {actual[0]}...")
return False
return True
2. Firmware Co-Simulation (The "Abstract Firmware")
Before boot code or drivers are ready, we often need to verify that register sequences work as expected. Writing these in raw UVM sequences is verbose and disconnected from the software view.
By mimicking the firmware API in Python, we can write tests that look like software drivers. These scripts can later serve as the specification for actual C firmware.
Python HAL Driver
# python_firmware/drivers/dma.py
class DMADriver:
"""Hardware Abstraction Layer - mirrors C firmware API"""
def __init__(self, bridge):
self.base_addr = 0x4000_0000
self.bridge = bridge
def transfer(self, src, dst, size):
# Configure source address
self.bridge.write(self.base_addr + 0x0, src)
# Configure destination address
self.bridge.write(self.base_addr + 0x4, dst)
# Set size and trigger start bit
self.bridge.write(self.base_addr + 0x8, size | 0x1)
# Poll for completion
while (self.bridge.read(self.base_addr + 0xC) & 0x1):
pass
return True
The benefit: Same test logic works in simulation, emulation, and post-silicon with only the bridge implementation changing.
3. System-Level Orchestration (The "Hub")
In full SoC verification, we often need to coordinate traffic across multiple interfaces (e.g., WiFi packets in, PCIe data out). Python creates an excellent Hub for this orchestration.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e0f2fe', 'primaryTextColor': '#0f172a', 'primaryBorderColor': '#0066cc', 'lineColor': '#475569', 'secondaryColor': '#f8fafc', 'tertiaryColor': '#ffffff'}}}%%
flowchart TB
subgraph PY["Python Test Orchestrator"]
direction LR
MGR["Traffic\nManager"]
WIFI_GEN["WiFi\nGenerator"]
PCIE_CHK["PCIe\nChecker"]
MGR --> WIFI_GEN
MGR --> PCIE_CHK
end
subgraph SV["SystemVerilog SoC Environment"]
direction TB
AG_WIFI[WiFi Agent]
AG_PCIE[PCIe Agent]
DUT{{"SoC DUT"}}
AG_WIFI ==> DUT
DUT ==> AG_PCIE
end
WIFI_GEN -.->|DPI: inject packets| AG_WIFI
AG_PCIE -.->|DPI: check output| PCIE_CHK
style DUT fill:#d1fae5,stroke:#10b981,stroke-width:2px
style MGR fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
style PY fill:#fffbeb,stroke:#d97706
Python orchestrates end-to-end scenarios while UVM agents handle protocol-level details.
4. Scalability: The GIL Consideration
The Global Interpreter Lock (GIL) prevents Python threads from running fully in parallel. In simulation, this is rarely the bottleneck—the simulator (Verilator/VCS) is usually slower than Python logic.
However, for intense computation (image processing, ML inference), consider:
- Multiprocessing: Spawn separate processes for heavy computation using
multiprocessingmodule - Batching: Send data in large arrays rather than word-by-word to amortize DPI call overhead
5. From Simulation to Emulation
A key architectural benefit is portability. The same Python test patterns can be reused across:
| Environment | Bridge Implementation |
|---|---|
| RTL Simulation | DPI-C direct calls |
| FPGA Emulation | Transactor over PCIe/UART |
| Post-Silicon Lab | JTAG/USB host interface |
This "Shift-Left" and "Shift-Right" capability maximizes ROI on test development.
This concludes our series on Python-driven Verification. We hope this inspires you to explore how modern software practices can modernize hardware verification!
Comments (0)
Leave a Comment