Python and the UVM

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 @cocotb.test() decorator:

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.

The 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 ConfigDB().

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.

Awaiting Tasks

The 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 await.

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 ALUtest extends 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.

The build_phase() function

The 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.

The 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.

The run_phase() coroutine

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 run_phase().

There is no phase argument, so the raise_objection() and drop_objection() functions are methods in uvm_component. The UVM will run until the every raise_objection has been matched by a drop_objection.

The 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.

The end_of_elaboration_phase() function

The 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.

Summary

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:

% 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.

Leave a Reply