Image
image
image
image


Design Articles

A Practical Guide to Adopting the Universal Verification Methodology—Part 3


This article is excerpted from Chapter 4: UVM Library Basics from the newly published book A Practical Guide to Adopting the Universal Verification Methodology by Sharon Rosenberg and Kathleen A. Meade (Copyright 2010 by Cadence Design Systems). This is the third of four weekly installments. If you can't wait to read the rest, the book is available at Amazon.com.

By Sharon Rosenberg and Kathleen A. Meade, Cadence Design Systems

uvm book 4.6 Transaction-Level Modeling in UVM

Over two decades ago, designers shifted from gate-level to RTL design. This shift was driven by the development of standard Verilog and VHDL RTL coding styles, as well as the availability of RTL synthesis and implementation tools. A major benefit of moving to RTL was that it enabled designers to focus more on the intended cycle level behavior designs and design correctness at this level, and much less on gate-level considerations.

Transaction-level modeling (TLM) is a similar shift in abstraction levels that has been occurring for both design and verification engineers. With transaction-level models, the focus is on modeling distinct transactions flowing through a system, and less on clock cycle level behavior.

TLM has been used within testbenches for many years. In general, any testbench is at the transaction-level if it has components that create stimulus or that do coverage or checks using transactions rather than using clock cycle level behavior. To verify RTL DUTs, such testbenches use transactors (sometimes called “bus functional models” or BFMs) to convert between the transaction level and the RTL. In UVM, such transactors are also called drivers and collectors.

In TLM, transactions in the system are modeled using method calls and class objects. There are several significant benefits that come from modeling at the transaction level rather than at the signal level:

  • TLM models are more concise and simulate faster than RTL models.

  • TLM models are at a higher level of abstraction, and more closely match the level of abstraction at which the verification engineer or design engineer thinks about the intended functionality. This makes it easier to write the models and easier for other engineers to understand them.

  • TLM models tend to be more reusable since unnecessary details which hinder reuse are moved outside of the models. Also, TLM enables use of object-oriented techniques such as inheritance and separation of interfaces from implementation.

The adoption of TLM is dependent on the availability of standard TLM modeling approaches, just as the adoption of RTL synthesis flows was dependent on the availability of standard RTL coding styles. Fortunately, in recent years, several important standardized TLM APIs have been defined. Two particularly prominent standards in the EDA and ESL area are the Open SystemC Initiative (OSCI) TLM 1.0 and TLM 2.0 standards.

The OSCI TLM 1.0 standard is a simple, general-purpose TLM API that is designed to model message passing. In message passing, objects (in this case transactions) are passed between components in a manner similar to packets being passed on a network.

In message passing as in packet transmission, there is no shared state between sender and receiver—rather, the only communication is contained within the messages that are passed.

The OSCI TLM 2.0 standard is designed to enable the development of high speed virtual platform models in SystemC. The TLM 2.0 standard is specifically targeted at modeling on-chip memory mapped buses, and contains many features to enable integration and reuse of components which connect to on-chip buses.

OSCI TLM 1.0 and TLM 2.0 are separate and distinct standards, each serving different purposes. Some people may assume that TLM 2.0 supersedes TLM 1.0 due to their naming, but this is not the case.

The UVM provides TLM APIs and classes which are based on the TLM 1.0 standard. This is because the general purpose message passing semantics of TLM 1.0 are well suited to the needs of transaction-level modeling for many verification components. TLM 1.0 is also well suited to modeling communication between different languages, for example between models in SystemVerilog, SystemC, and e. The TLM 1.0 interfaces in UVM can even be used to communicate with TLM 2.0 models in SystemC.

This section presents some of the key concepts for using TLM within UVM so that you can understand how to use TLM to construct reusable verification components. More detailed information on the various TLM classes in UVM is also available within the UVM Reference Manual.

4.6.1 Key TLM Concepts in UVM

4.6.1.1 Modeling Transactions

In UVM, a transaction is any class that extends from uvm_sequence_item. A transaction is defined by the user to contain whatever fields and methods are required to model the information that needs to be communicated between different components within the verification environment. For example, a simple packet might be modeled as follows:

  1. class simple_packet extends uvm_sequence_item;
  2.  rand int src_addr;
  3.  rand int dst_addr;
  4.  rand byte unsigned data[];
  5.  constraint addr_constraint { src_addr != dst_addr; }
  6. ...
  7. endclass

A transaction typically contains sufficient data fields to enable a driver or transactor to create the actual signal-level activity that is required to represent the transaction data. A transaction may also contain additional data fields to control how randomization occurs, or for any other purposes within the verification environment. You can extend from transactions to include additional data members, functions, and constraints. Later sections show how you can use the concept of extending from transactions to accomplish specific verification tasks with minimum effort.

4.6.1.2 TLM Ports and Exports

The TLM API in UVM specifies a set of methods that are called by models to communicate transactions between components. In UVM, a port object specifies the set of methods that can be called, while an export object provides the implementation of the methods. Ports and exports are connected together via connect() calls when the verification environment is constructed, and subsequent calls to the TLM methods on the port will execute the TLM methods implemented within the export.

Example 4.7: Transaction from a Producer to a Consumer Using put

In UVM TLM, the put interface can be used to send a transaction from a producer to a consumer. A simple example of such a producer in UVM follows:

class producer extends uvm_component;
  uvm_blocking_put_port #(simple_packet) put_port;

  function new(string name, uvm_component parent);
    put_port = new("put_port", this);
  endfunction

  virtual task run();
    simple_packet p = new();
    ..
    put_port.put(p);
  endtask
endclass

As mentioned before, the put port is connected via a connect() call to a put export. The implementation of the put method that is called above is provided in the consumer component:

class consumer extends uvm_component;
  uvm_blocking_put_imp #(simple_packet, consumer) put_export;

  task put(simple_packet p);
    // consume the packet
  endtask
endclass

The result of connecting the port to the export and then calling the put method in the producer above is that the put implementation in the consumer will execute with the simple_packet object passed from producer to consumer.

TLM also include standard graphical notation to illustrate different type of communication. The put communication flow is captured in the block diagram below:

figure 4-2
Figure 4-2: Simple Producer/Consumer put Communication

The TLM interfaces in UVM specify some simple rules that the producer and consumer both must follow. In this case, for the put interface, the rules are:

  • The implementation of the put method may block execution until the put completes. Therefore, the caller of the put method must work properly if the implementation of the put method blocks execution.

  • The producer is responsible for creating the packet transaction, and the consumer must not modify it (if it needs to it must copy it first).

By following these rules, it becomes possible to easily swap either the producer or consumer with alternative models that also adhere to the same simple TLM interface. The TLM API serves as a simple interface contract that promotes interoperability, much as standards such as USB and Ethernet do in the hardware world. By allowing easy swapping of models, UVM TLM plays a key role in enabling you to reuse models and meet verification goals, as we see in later chapters.

In the above example, there is a single process that runs in the producer, and when the put method is called, the flow of control passes to the put task within the consumer. The put method passes the transaction in the same direction as the control flow of the put method call itself.

In some modeling situations, it is necessary to have the transaction data flow happen in the opposite direction from the control flow of the TLM method call, because the consumer contains the process that requires the transaction data. A producer/consumer example in this case would instead use the get interface, and would be written as:

    1. class producer_2 extends uvm_component;
    2.   uvm_blocking_get_imp #(simple_packet, producer_2) get_export;
    3.   task get(output simple_packet p);
    4.     simple_packet p_temp = new();
    5.     ...
    6.     p = p_temp;
    7.   endtask
    8. endclass
    9. class consumer_2 extends uvm_component;
    10.   uvm_blocking_get_port #(simple_packet) get_port;
    11.   function new(string name, uvm_component parent);
    12.     get_port = new("get_port", this);
    13.   endfunction
    14.   virtual task run();
    15.     simple_packet p;
    16.     ...
    17.     get_port.get(p);
    18.   endtask
    19. endclass

As in the case of the put interface above, UVM specifies rules that the producer and consumer must follow while using the get interface:

  • The implementation of the get method may block execution until the get completes. Therefore, the caller of the get method must work properly if the implementation of the get method blocks execution.

  • The implementation of the get method must create and return a new simple_packet object to the caller of get.

The graphical notation for get communication scheme looks like this:

figure 4-3
Figure 4-3: Consumer gets from Producer

4.6.1.3 Connecting Ports to Exports

In the example above, the port-to-export connection needs to be created by calling the connect() method. To make the connection, the user needs to invoke the connect method within the user-supplied connect callback in the parent component of the producer and consumer components:

class parent_comp extends uvm_component;
producer producer_inst;
consumer consumer_inst;
  ...
  virtual function void connect();
    producer_inst.put_port.connect(consumer_inst.put_export);
  endfunction
endclass

The general rule is that when connecting ports to exports, the connect method of the child component port is always invoked with the child component export as the argument.

4.6.1.4 Connecting Ports to Ports and Exports to Exports

In Verilog RTL, modules have ports which represent their signal level interface to the outside world. Verilog RTL modules also may contain internal structure such as child modules, which themselves may have signal ports. However, it is the ports that exist on the parent module which represent the interface specification of the parent module—any child modules and ports of the child modules are considered implementation details that should be kept hidden.

Similarly, in UVM TLM, it is the ports and exports of a given component that represent that component's TLM interface to the outside world. Any child components and ports and exports of such child components should be considered implementation details that should be hidden. Such hiding of internal structure enhances modularity of the overall verification environment and enables easier reuse and swapping of particular components.

But what if you have a port or an export on a child component within a parent component that needs to be made visible as a port or an export of a parent component? In this case, you need to connect a child port to a parent port, or a child export to a parent export.

Example 4-8: Connecting a Child Port to a Parent Port

class parent_producer extends uvm_component;
  uvm_blocking_put_port #(simple_packet) put_port;
  producer child_producer_inst;

  function new(string name, uvm_component parent);
    put_port = new("put_port", this);
    child_producer_inst = new("child_producer_inst", this);
  endfunction

  virtual function void connect();
    child_producer_inst.put_port.connect(put_port);
  endfunction
endclass

The general rule is that when connecting child ports to parent ports, the connect() method of the child port is called with the parent port as the argument.

Example 4-9: Connecting a Child Export to a Parent Export

class parent_consumer extends uvm_component;
  uvm_blocking_put_export #(simple_packet) put_export;
  consumer child_consumer_inst;

  function new(string name, uvm_component parent);
    put_export = new("put_export", this);
    child_consumer_inst = new("child_consumer_inst", this);
endfunction

  virtual function void connect();
    put_export.connect(child_consumer_inst.put_export);
  endfunction
endclass

The general rule in this case is that when connecting child exports to parent exports, the connect() method of the parent export is called with the child export as the argument. Note this is different from the connection rule for child port to parent port immediately above.

4.6.1.5 Using uvm_tlm_fifo

In the original producer and consumer example above, there is a single process located in the producer, and the consumer does not contain any process—rather, the put method in the consumer is executed when put is called in the producer.

In the subsequent producer_2 and consumer_2 example above, there is a process in consumer_2, and consumer_2 has a get_port that it calls to get each packet.

You might encounter modeling situations where you need to connect components such as the first producer with components such as consumer_2. Such situations arise frequently because many TLM components require their own processes for modeling purposes, and yet they still must be connected together to pass transactions.

How can components such as producer and consumer_2 be connected? A very common way to connect them is to use the uvm_tlm_fifo component within UVM. The UVM uvm_tlm_fifo is a parameterized FIFO that has both put and get exports. The uvm_tlm_fifo is instantiated with a parameter that indicates the type of objects that are stored in the fifo, and the constructor for uvm_tlm_fifo has an argument that indicates the maximum depth of the fifo (which defaults to one).

Example 4-10: uvm_tlm_fifo Usage

class producer_consumer_2 extends uvm_component;
  producer producer_inst;
  consumer_2 consumer2_inst;
  uvm_tlm_fifo #(simple_packet) fifo_inst;   // fifo stores simple_packets

  function new(string name, uvm_component parent);
    producer_inst = new("producer_inst", this);
    consumer2_inst = new("consumer2_inst", this);
    fifo_inst = new("fifo_inst", this, 16);  // set fifo depth to 16
  endfunction

  virtual function void connect();
    producer_inst.put_port.connect(fifo_inst.put_export);
    consumer2_inst.get_port.connect(fifo_inst.get_export);
  endfunction
endclass

When this model runs, the process in the producer component creates and stores packets into the fifo, and the process in the consumer_2 component consumes the packets when it calls the get method.

Because of the fifo, the synchronization between the two processes is now decoupled. Either process may execute arbitrary delays, yet the usage of the fifo and the blocking put and get calls ensure that no packets are ever lost. In many verification modeling situations, this insensitivity to various delays within models and guaranteed delivery of transactions is desirable. The UVM TLM makes it easy to model such systems.

The figure below illustrates a fifo connection.

figure 4-4
Figure 4-4: Using a uvm_tlm_fifo

4.6.1.6 Analysis Ports and Exports

The put and get ports described so far require that exactly one export be connected to them before simulation starts. If the ports are left unconnected, you get an error message from UVM that tells you to connect them.

Sometimes when you are constructing components such as monitors, you may need a port that can either be left unconnected, or connected to one or more components. This is because monitors are typically passive components in the overall verification environment, and do not directly affect the generation of stimulus or affect any synchronization with the DUT. Instead, monitors passively collect data transactions and pass them on to any other components which have registered an interest in the data transactions.

Analysis ports exist in UVM for this type of situation. Analysis ports are similar to regular TLM ports, but it is okay to leave them unconnected, and they also allow any number of analysis exports to be connected to them.

For those familiar with callbacks, analysis ports are essentially structured callbacks (callbacks with port connectivity).

An analysis port has a single void function write() that can be called with a single transaction argument. Every analysis port maintains a list of analysis exports that have been connected to it. When the write method of an analysis port is called with a transaction, the analysis port calls the write method of every connected analysis export with the same transaction. Because the write method is a function, it is guaranteed that the analysis port write will return without blocking. And, because the write method is a void function, the component containing the analysis port does not have any status returned after the write is done. The overall effect is that from the perspective of the component containing the analysis port, one does not need to know or care about any downstream components that may be connected to the analysis port.

Example 4-11: Monitor with an Analysis Port

class packet_monitor extends uvm_component;
  uvm_analysis_port #(simple_packet) analysis_port;

  function new(string name, uvm_component parent);
    analysis_port = new("analysis_port", this);
  endfunction

  virtual task run();
    simple_packet p = new();
    .. // reassemble packet here from lower level protocol
    analysis_port.write(p); // write the collected packet to the analysis port
  endtask
endclass

Example 4-12: Component with an Analysis Export

class packet_checker extends uvm_component;
  uvm_analysis_imp #(simple_packet, packet_checker) analysis_export;

  function new(string name, uvm_component parent);
    analysis_export = new("analysis_export", this);
  endfunction

  function void write (simple_packet p);
    // check the packet here
  endfunction
endclass

These two components can be instantiated in a parent component and the analysis port and export can be connected using the normal UVM TLM connection rules. And, as mentioned above, since the analysis port allows more than one export to be connected to it, it is okay to instantiate multiple components that have analysis exports and connect them to the analysis port within the packet_monitor component.

Sometimes the transactions that are being passed through an analysis port cannot be processed immediately by the downstream component. Instead, they may need to be stored for some period of time before they can be consumed. For example, this situation might arise in a scoreboard which needs to compare actual packets coming from a DUT versus expected packets coming from a reference model. In this case the packets coming from the reference model may need to be stored since the packets from the DUT will encounter various delays.

The uvm_tlm_fifo might seem like a good way to solve this sort of problem, by storing the packets until they are needed. However, uvm_tlm_fifo does not have an analysis export, so you cannot directly connect it to an analysis port. There is a good reason for this—any implementation of the write method in an analysis export must pass the transaction and return immediately, but this is not always possible if the fifo has a fixed size. UVM has the uvm_tlm_analysis_fifo to address this need. The uvm_tlm_analysis fifo has an analysis export, so that it can be directly connected to analysis ports, and it has an unbounded size so that writes always succeed.

figure 4-5
Figure 4-5: Analysis Communication

4.6.1.7 `uvm_*_imp_decl Macros

Sometimes there are situations where a component needs to have multiple implementations of the same interface. A classic example of this is a scoreboard that has to monitor multiple interfaces (for instance, two inputs and one output). When such situations arise, you must provide some means for dealing with the multiple interfaces. There are three potential solutions:

  • Create a component for each implementation with that component having responsibility for the specific interface.

  • If the transaction type is the same for each interface, use a single implementation; this requires that the transaction object provide some way of disambiguating where the transaction came from.

  • Create additional _imp types for each port where each _imp type calls to a different implementation function.

In UVM, the third option is the simplest because of the `uvm_*_imp_decl macros. These macros are used to create new implementation types which forward to different implementation functions. For example, if you use `uvm_analysis_imp_decl(_1), you will get a new implementation class call uvm_analysis_imp_1#(type T) which will have an implementation function called write_1().

Tip: Guidelines for using the `uvm_*_imp_decl macros:

  • Because these declarations create new class types, it is best to put the macros in some shared scope, such as a package.

  • Use these classes in cases where a component really needs to have to implementations of the same interface (as with a typical scoreboard).

  • Use generic, but meaningful names for the suffix. For example, _inport1 and _outport1 are reasonable names because they provide some information about the connectivity of the implementation.

  • Start the suffix with an underscore so that the implementation functions will have the underscore separator (for example, write_inport1).

Example 4-13: `uvm_*_imp_decl Macros

Below is a simple example of a scoreboard which uses the macros.

    1. package my_analysis_imps_pkg;
    2.   import uvm_pkg::*;
    3.   `include "uvm_macros.svh"
    4.   `uvm_analysis_imp_decl(_inport1)
    5.   `uvm_analysis_imp_decl(_inport2)
    6.   `uvm_analysis_imp_decl(_outport2)
    7.   `uvm_analysis_imp_decl(_outport2)
    8. endpackage: my_analysis_imps_pkg
    9. package scoreboard_pkg:
    10.   import uvm_pkg::*;
    11.   `include "uvm_macros.svh"
    12.   import my_analysis_imps_pkg::*;
    13.   import mytx_pkg::*;
    14.                      mytx q1[$], q2[$];
    15.                      class myscoreboard extends uvm_component;
    16.     uvm_analysis_imp_inport1#(my_tx, myscoreboard) in_export_1;
    17.     uvm_analysis_imp_inport2#(my_tx, myscoreboard) in_export_2;
    18.     uvm_analysis_imp_outport1#(my_tx, myscoreboard) out_export_1;
    19.     uvm_analysis_imp_outport2#(my_tx, myscoreboard) out_export_2;
    20.     function new(string name, uvm_component parent);
    21.        super.new(name,parent);
    22.        in_export_1 = new("in_export_1", this);
    23.        in_export_2 = new("in_export_2", this);
    24.        out_export_1 = new("out_export_1", this);
    25.        out_export_2 = new("out_export_2", this);
    26.     endfunction
    27.     function void write_inport1(mytx t);
    28.       q1.push_back(t);
    29.                                    endfunction
    30.     function void write_inport2(mytx t);
    31.       q2.push_back(t);
    32.     endfunction
    33.     function void write_outport1(mytx t);
    34.       mytx t1, t2;
    35.       t1 = q1.pop_front();
    36.       t2 = t.transform1(); //execute some transformation function
    37.       if(!t1.compare(t.transform1()))
    38.         `uvm_error("SBFAILED", $psprintf("Expected: %s  Got: %s",                                                                                                                                                         t1.sprint(), t2.sprint()))
    39.     endfunction: write_outport1
    40.     function void write_outport2(mytx t);
    41.       mytx t1, t2;
    42.       t1 = q2.pop_front();
    43.       t2 = t.transform2(); //execute some other transformation function
    44.       if(!t1.compare(t.transform1()))
    45.         `uvm_error("SBFAILED", $psprintf("Expected: %s  Got: %s",                                                                                                                                                         t1.sprint(), t2.sprint()))
    46.     endfunction
    47.   endclass
    48. endpackage: scoreboard_pkg;

Next Week: 4.7: The UVM Factory

As part of the verification task and in order to follow the verification plan, a user may need to extend the generic verification environment behavior beyond its original intent. Unlike design, where specifications can capture the desired functionality in a complete and deterministic form, the verification process is fluid, dynamic and unpredictable.

Bookmark and Share


Insert your comment

Author Name(required):

Author Web Site:

Author email address(required):

Comment(required):

Please Introduce Secure Code:


image
image