Thought Leadership

The configuration database in pyuvm

The configuration database

In the previous post in the Python for Verification Series we discussed how pyuvm implemented TLM 1.0. Now we turn our attention to some of the UVM’s utilities and how we use them in Python. The first of these is the UVM configuration database.

The configuration database as defined in the IEEE 1800.2 specification is very complicated. It includes a uvm_resource_db that handles storing data of different types with the uvm_config_db as a user interface for the uvm_resource_db.

None of this was necessary for pyuvm since there are no types, so I refactored it and named it ConfigDB to avoid confusion with the SystemVerilog version. In this post we’ll examine ConfigDB().

What is the ConfigDB()?

The ConfigDB class is a singleton database that stores objects using keys. It is much like a Python dictionary except that the keys must be strings and we can control the visibility to the data using the UVM hierarchy.

In this blog post I refer to the configuration database as the ConfigDB() as a reminder that we always call the ConfigDB class (using parentheses) to get the handle to the singleton.

Basic Setting and Getting

We’ve already seen the most basic use of ConfigDB() in the Python and the UVM introductory blog post. We call our UVM testbench using a cocotb test. The test takes the dut argument and uses it to create an instance of TinyAluBFM then it stores the bfm in the ConfigDB() using the label BFM:

@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")

We see here that the pyuvm version of ConfigDB() is much simpler than the SystemVerilog one since we don’t have to deal with parameterized types or a litany of static function calls. Instead we simply say ConfigDB().set().

We get the BFM from ConfigDB() in, for example, the Driver class. Here we get the bfm in the connect_phase():

class Driver(uvm_driver):
    def connect_phase(self):
        self.bfm = ConfigDB().get(self, "", "BFM")

    async def run_phase(self):
        while True:
            command = await self.seq_item_port.get_next_item()
            await self.bfm.send_op(command.A, command.B, command.op)
            self.logger.debug(f"Sent command: {command}")
            self.seq_item_port.item_done()

This approach of using None, '* when setting the data and self, "" when getting the data is how we store global data. We’ll now look at storing data at specific places in the hierarchy.

UVM hierarchical control

We use the ConfigDB() instead of a dictionary so that we can instantiate the same component class in different parts of the testbench, and have them retrieve different data using the same label. That way we don’t have to modify the code or somehow pass the label to the object.

The ConfigD() uses a combination of a handle to a UVM component and a string to create a UVM path both to store data in the ConfigDB() and to retrieve it. The first two arguments to both the set() and get() functions are the component and the string path. Here is how this applies to the above call to set():

    ConfigDB().set(None, "*", "BFM", bfm)

The UVM component in this case is set to None. This gives us an empty base path of "". The path string in this case is "*" and so this means we store "BFM" with the path "*", which matches everything.

The call to get() looks like this:

self.bfm = ConfigDB().get(self, "", "BFM")

In this case the self variable contains an instance of the Driver. The get() function uses get_full_name() on the self variable to get the path to driver: "uvm_test_top.env.driver". Then it concatenates the string in the second argument ("") to add nothing to the base path.

Finally the ConfigDB() uses the Python fnmatch function to see if the path in set() matches the path in get(). u"*" matches uvm_test_top.env.driver and so we read the data.

We use this system to store different data, say different BFMs, in different parts of the testbench hierarchy. Here is a simple example:

We create a simple component that pulls a message out of the ConfigDB() and logs it:

class MsgLogger(uvm_component):

    def build_phase(self):
        self.msg = ConfigDB().get(self, "", "MSG")

    async def run_phase(self):
        self.raise_objection()
        self.logger.info(self.msg)
        self.drop_objection()

Now we’ll instantiate this component in two places.

class MsgEnv(uvm_env):
    def build_phase(self):
        self.loga = MsgLogger("loga", self)
        self.logb = MsgLogger("logb", self)

Finally we’ll use the ConfigDB() to pass two different messages:

class LogTest(uvm_test):

    def build_phase(self):
        self.env = MsgEnv("env", self)
        ConfigDB().set(self, "env.loga", "MSG", "AAAAA")
        ConfigDB().set(self, "env.logb", "MSG", "BBBBB")

We’ve used self as the context object. The LogTest object will be instantiated with the name uvm_test_top so that will be the base path for storing the data. If we ever use LogTest as part of a larger testbench the ConfigDB() will work same way because self would generate the full path to the place we were instantiated.

In this case, the self path is uvm_test_top and we use the path string in the second argument to create uvm_test_top.env.loga and uvm_test_top.env.logb. Now each instance of MsgLogger will get a different message:

INFO: testbench.py(12)[uvm_test_top.env.loga]: AAAAA
INFO: testbench.py(12)[uvm_test_top.env.logb]: BBBBB

Path overrides

One can imagine a situation where a UVM components sets a value in the ConfigDB() at a place in its hierarchy, but then gets instantiated inside another component that sets a different value to the same path.

The IEEE 1800.2 UVM specification describes what to do in this situation: If the components set the configuration values in the build_phase() function then the higher level component’s set() overrides the lower level component’s set().

For example, let’s have the environment set a value for loga:

class MsgEnv(uvm_env):
    def build_phase(self):
        self.loga = MsgLogger("loga", self)
        self.logb = MsgLogger("logb", self)
        ConfigDB().set(self, "loga", "MSG", "AAAENVAAA")

This means that "AAAENVAAA" is set for uvm_test_top.env.loga. Remember, however that LogTest has set a different value ("AAAAA") to the same path.

class LogTest(uvm_test):

    def build_phase(self):
        self.env = MsgEnv("env", self)
        ConfigDB().set(self, "env.loga", "MSG", "AAAAA")
        ConfigDB().set(self, "env.logb", "MSG", "BBBBB")

The result? The test wins since it is the higher level component.

INFO: testbench.py(12)[uvm_test_top.env.loga]: AAAAA
INFO: testbench.py(12)[uvm_test_top.env.logb]: BBBBB

Debugging the ConfigDB()

The ConfigDB() has several debugging mechanism. One comes from the UVM specification. You can set the ConfigDB().is_tracing class variable to True:

@cocotb.test()
async def log_msgs(dut):
    """Exercise hierarchy storage"""
    ConfigDB().is_tracing = True
    await uvm_root().run_test("LogTest")
    print(ConfigDB())
    assert True

Now the ConfigDB() will print out all the set() and get() operations. Notice that we see both set() calls using the same path.

# CFGDB/SET Context: uvm_test_top  --  uvm_test_top.env.loga MSG=AAAAA
# CFGDB/SET Context: uvm_test_top  --  uvm_test_top.env.logb MSG=BBBBB
# CFGDB/SET Context: uvm_test_top.env  --  uvm_test_top.env.loga MSG=AAAENVAAA
# CFGDB/GET Context: uvm_test_top.env.loga  --  uvm_test_top.env.loga MSG=AAAAA
# CFGDB/GET Context: uvm_test_top.env.logb  --  uvm_test_top.env.logb MSG=BBBBB

And as final bonus, you can print ConfigDB() as we see in the last line of the test. This gives the database. You can see how the ConfigDB() has prioritized the two conflicting values:

# PATH                : FIELD     : DATA                          
# uvm_test_top.env.loga: MSG       : {999: 'AAAAA', 998: 'AAAENVAAA'}
# uvm_test_top.env.logb: MSG       : {999: 'BBBBB'}

Summary

In this blog post we examined the ConfigDB(), a refactored Python version of the uvm_config_db and the uvm_resource_db. We saw how to store values globally and at a specific location in the UVM hierarchy.

We also saw how the UVM specification defined the response to conflicting values for a given key with the same path, and the prioritization behavior that happens only in the build_phase.

Finally we saw how to debug the ConfigDB() using the is_tracing variable and the print() function

Leave a Reply

This article first appeared on the Siemens Digital Industries Software blog at https://blogs.stage.sw.siemens.com/verificationhorizons/2021/10/27/the-configuration-database-in-pyuvm/