Global training solutions for engineers creating the world's electronics

UVM-style Configuration with VHDL

Emulating some of the features of the Universal Verification Methodology with VHDL

We ran a webinar looking at how certain features of UVM can be emulated using VHDL. In particular, how can VHDL be configured?

The basic idea is that as a verification engineer or designer, you may want to run a whole set of tests one after another, such as when performing regressions. Regressions are used to see if a bug that was once fixed has re-occurred (i.e. the design has regressed or gone backwards). To do this it's very convenient if you can run multiple tests with different parameters, but without having to edit and re-compile code.

Between VHDL and the simulation tools, there are various options

 

  • Use top-level generics
  • Use Tool Command Language (Tcl)
  • VHDL configurations
  • Use some kind of database of settings

If you want to know about the first three, we will be running the webinar again in the future - and when you register for our webinars you can download the presentation to view later.

Using a database for configuring values

Here, however, we are going to briefly present and provide the code for the last option - a kind of database. The idea is that the user can configure constants, initial values of signals, and initial values of variables by retrieving values from a database. In fact values could be read in during simulation (not just at initialization). From the users' point of view, they can look up values using some kind of unique ID for the object in the design that they want to initialize.

Luckily VHDL provides a way of creating a unique ID, using so-called "general attributes":

 a'SIMPLE_NAME     -- String name of a
 a'INSTANCE_NAME   -- string hierarchical path
 a'PATH_NAME       -- string hierarchical path excluding instance information 

Here are some examples of typical output, in the order used above:

 a
 :tb(bench):u2@e2(a):
 :tb:u2 

What this means is that we can create a database file like this:

 :tb(bench):u1@e1(a)::debug
true 
:tb(bench):monitor:i
200

which consists of key/value pairs. The key is the instance name (returned by 'instance_name) of the item we want to set, and the following line is the value. We then can write a package to read in this file and make it easy to use. Here's an example of how the package can be used:

use work.configpack.all;
architecture a of e1 is 
begin

   process
     variable debug : boolean;
   begin
     debug := configpack.get(debug'INSTANCE_NAME);
     -- ...
end architecture a;

The code for the configuration package is available for download. What it does is:

 

  • it reads in a file of key/value pairs
  • it parses it
  • it stores it an array of strings
  • it allows the user to access values via overloaded get functions

We could use protected types, but it's a bit easier to read and understand a package, plus it has the advantage that it works with any simulator that has support for at least VHDL 1076-1993.

From the user's point of view, they include a package that looks like this:

package configpack is 
  -- false means write debug messages
  constant nDebug : boolean := true; 
  constant configArraySize : POSITIVE := 100; 
  constant configPathLength : positive := 200; 
  constant configValueLength : positive := 20; 
  constant configFileName : string := "config.txt";   
  
  impure function get(key: string) return integer;
  impure function get(key: string) return boolean;
  impure function get(key: string; length : positive) return string; 

end package configpack;

Notice the overloaded get functions.

 

To give you an idea of the code, here's an extract from the package body, where the various data variables and constants are set:

use std.textio.all;
use work.textutils.all;
package body configpack is 
  
  type configInfoT is record
    path : string(1 to configPathLength);   -- lookup key
    value : string(1 to configValueLength); -- value
  end record configInfoT;
    
  type configInfoArrayT is array
    (1 to configArraySize) of configInfoT;

  constant nConfigItems : natural :=
    getFileLineCount(configFileName) / 2;
  
  impure function getConfigInfo(configFileName : string)... 
  
  constant ConfigInfo : configInfoArrayT :=  getConfigInfo(configFileName);   

  impure function get(key: string) return integer ...

Note how we use functions and constants which are declared in a package body, but invisible to the outside world. This is an example of data hiding or encapsulation in software jargon. The user of the package doesn't have to know about these internal details, and can't see them as they are not made visible in the package itself.

The configuration package body makes use of some text utilities for trimming, normalising, and matching strings. These utilities are in the package textutils.

So if the user creates a file containing:

  *monitor:i
200

what happens when they call get? Below you'll see the code of the function that retrieves integer values:

impure function get(key: string) return integer is
  variable matchCnt : natural := 0;
  variable value : integer;
begin
  for I in 1 to nConfigItems loop
    if match(trim(configInfo(I).path), key) then
      matchCnt := matchCnt + 1;
      value := INTEGER'VALUE(configInfo(I).value);
      print("match found" & key);
    end if;
  end loop;
  assert matchCnt /= 0 report "ERROR: no matches for path  " & key;
  assert matchCnt <= 1 report "ERROR: More than one match for path " & key;
  return value;
end function;

This function has various advanced features, mainly that it is impure. An impure function can return a different value each time it is called (since it accesses an array of data declared outside itself). The function also does a reasonable amount of error checking.

We can also show you here a snippet of the database loading code. This is the code that reads in the database file and stores the keys (instance names) and values in a array of records. Each record contains two strings, one for the key and one for the value. The reading code must cope with variable length data, and the trick is to use L.all to refer to the contents of a line buffer when using textio. This is because a variable of type line is actually of type access string which is a VHDL way of declaring a pointer. To get at the data pointed at, in C you would write *L. In VHDL you use instead L.all. We also make extensive use of our text utility functions for trimming and normalising strings - and the match function which has the nice feature that it accepts a wildcard. Here is the code for reading the file:

-- expect pairs of lines, path followed by value
for i in 1 to nConfigItems loop
  if not endfile(F) then
    readline(F, L);
    assert L'LENGTH > 0 report "ERROR: Empty line in file ";
    
    configData(I).path(1 to trim(L.all)'LENGTH) := trim(L.all);
    readline(F, L);
    assert L'LENGTH > 0 report "ERROR: Empty line in file ";

    configData(I).value(1 to trim(L.all)'LENGTH) := trim(L.all);
  end if;
end loop;

As you can see there's quite a lot of code, so now probably the best thing is for you to download and experiment with it yourself. If you follow the download link, you'll find a zip file containing the code for the two packages, and a very simple example of usage.

Click here to get the code. In exchange, we will ask you to enter some personal details. To read about how we use your details, click here. On the registration form, you will be asked whether you want us to send you further information concerning other Doulos products and services in the subject area concerned.

Great training!! Excellent Instructor, Excellent facility ...Met all my expectations.
Henry Hastings
Lockheed Martin

View more references