In our previous two posts in this series on Python as a verification language, we examined Python coroutines and using coroutines to create cocotb bus functional models. Now we are going to look at the next step, the Universal Verification Methodology (UVM) implemented in Python.
The UVM is completely described in the IEEE 1800.2-2020 standard. The standard describes the UVM’s classes and functions and also describes some of the detail of how it has been implemented in SystemVerilog.
I use the IEEE UVM standard to create pyuvm, a clean-sheet reimplementation of the UVM from the standard. Refactoring the UVM in Python allowed me to simplify the code and take advantage of Python features such as multiple inheritance that made, for example, the
_imp classes unnecessary.
The initial release, pyuvm 1.0 implemented the UVM using Python threads. However, once cocotb released coroutine versions of Python Queues, it became clear that a cocotb-based pyuvm would be much easier to use. We’ll examine pyuvm 2.0 in this blog post.
Focusing on differences
This article assumes that the reader is familiar with the SystemVerilog version of the UVM. It will use an example testbench for the TinyALU as a path through a UVM testbench written in Python.
Python, with its lack of typing and dynamic nature, is easier to write than SystemVerilog, and so I refactored much of the UVM to take advantage of Python’s features. We’ll see these changes as we work through the testbench.
The cococtb test
We’ll start a the top of the testbench with the coroutine that we’ve tagged with the
import cocotb from pyuvm import * @cocotb.test() async def test_alu(dut): bfm = TinyAluBfm(dut) ConfigDB().set(None, "*", "BFM", bfm) await bfm.startup_bfms() await uvm_root().run_test("AluTest")
The first thing to note is the way we imported pyuvm. When we work in SystemVerilog we write
import uvm_pkg::* which makes all the UVM classes and functions available in our code without referencing the package. We’re doing the same here in Python.
The test starts simply enough. We create a
bfm object that contains the bus functional models we created in the previous blog post. The next line is from pyuvm and demonstrates the simplicity of writing UVM code in Python: we store the BFM in the configuration database.
ConfigDB().set(None, "*", "BFM", bfm)
Storing an item in the
config_db in the SystemVerilog UVM entails using a long incantation with various types and static function calls. Under the hood the
uvm_config_db interfaces is built upon the
uvm_resource_db which has a complicated way of storing a variety of data types in a database.
pyuvm does away with all this. Instead of static function calls, it provides a singleton that stores data in the configuration database using the same hierarchy-based control as the SystemVerilog version.
ConfigDB() call looks like an instantiation, but it is actually returning the handle to a singleton. Python doesn’t require an explicit
get() static method to implement singletons.
The result is a call to
ConfigDB().set() without the incantations needed to store values of a specific type. Any object can be stored anywhere in the pyuvm
The next thing we do in the test is launch the BFMs using the
bfm.startup_bfms() coroutine. The coroutines forks off the BFM coroutines as we saw in the bus functional model blog post.
We see in the last line that the UVM task
run_test() is now a coroutine. We get a handle to the
uvm_root() singleton and await
run_test(), passing the name of the test so the factory can create it. We’ll see the AluTest class defined below.
await call to
run_test() demonstrates a nice feature of Python: we call tasks and functions differently.
In SystemVerilog, we call a function that returns
void and a task the same way. There is nothing to indicate that the call could consume time. Python requires that all time-consuming calls be done using
The UVM test: ALUTest
Our call to
run_test() above took the string
ALUTest as an argument, which means we should look at
ALUTest for more differences between Python UVM and SystemVerilog UVM:
class AluTest(uvm_test): def build_phase(self): self.env = AluEnv.create("env", self) async def run_phase(self): self.raise_objection() seqr = ConfigDB().get(self, "", "SEQR") bfm = ConfigDB().get(self, "", "BFM") seq = AluSeq("seq") await seq.start(seqr) await ClockCycles(bfm.dut.clk, 50) # to do last transaction self.drop_objection() def end_of_elaboration_phase(self): self.set_logging_level_hier(logging.DEBUG)
The first thing to notice is that
uvm_test. Though Python style prefers camel casing for classes, pyuvm preserves all the class names from the standard. The exception is when, as with
ConfigDB() the feature has been completely refactored.
Notice that we don’t have the
\`uvm_component_utils() macro or its equivalent in the code. This is because pyuvm automatically registers all classes that extend
uvm_void in the factory.
build_phase() function demonstrates that pyvum implements what the UVM spec calls the
common_phases. The lack of a
uvm_phase phase argument in
build_phase() shows that pyuvm refactors phasing. It only implements the common phases and there is no mechanism to create custom phases, as this feature is rarely used.
build_phase() function creates an environment named
self.env using the factory. The call to the factory is much simpler than it is in SystemVerilog. We don’t have the heroic string of static function calls and variables in a SystemVerilog UVM factory incantation. Instead,
create is simply a static method in all classes that extend
uvm_object. The factory implements all the override functionality found in the SystemVerilog UVM.
In SystemVerilog UVM the
run_phase() is the only phase implemented as a time-consuming task. The same is true in pyuvm, as
run_phase() is implemented as a coroutine.
This means that you must use the
async keyword to define the
There is no
phase argument, so the
drop_objection() functions are methods in
uvm_component. The UVM will run until the every
raise_objection has been matched by a
run_phase() creates a sequence of type
AluSeq and gets the sequencer out of the
ConfigDB(). Then it uses
await to kick off the
start() task in the sequence.
end_of_elaboration_phase() function runs after the UVM has built the testbench hierarchy and connected all the FIFOs. In this example, we use it to set our logging level.
Notice that pyuvm does not implement the UVM reporting system, as Python provides the
logging module. Instead, pyuvm leverages
logging and provides hierarchical functions for us to control logging behavior.
This blog post introduced pyuvm, a Python implementation of the UVM built upon cocotb. While pyuvm implements all the features we expect from the UVM including the config db and the factory, it refactors elements of the UVM that are easier to use in Python.
You can see the source code for pyuvm at https://github.com/pyuvm/pyuvm.
You can install pyuvm from pypi.org using
% pip install pyuvm
This will also install cocotb
The next blog post in the series will examine logging in more detail, as well as pyuvm‘s implementation of TLM 1.0.