Most digital designs are done at a higher level of abstraction like RTL, although at times it becomes intuitive to build smaller deterministic circuits at a lower level by using combinational elements like and and or. Modeling done at this level is usually called gate level modeling as it involves gates and has a one to one relation between a hardware schematic and the Verilog code.

Verilog supports a few basic logic gates known as primitives as they can be instantiated like modules since they are already predefined.

## And/Or/Xor Gates

These primitives implement an AND and an OR gate which takes many scalar inputs and provide a single scalar output. The first terminal in the list of arguments to these primitives is the output which gets updated whenever any of the inputs change.

```  ```

module gates (	input a, b,
output c, d, e);

and (c, a, b); 	// c is the output, a and b are inputs
or  (d, a, b);	// d is the output, a and b are inputs
xor (e, a, b); 	// e is the output, a and b are inputs
endmodule

```
```
```  ```

module tb;
reg a, b;
wire c, d, e;
integer i;

gates u0 ( .a(a), .b(b), .c(c), .d(d), .e(e));

initial begin
{a, b} = 0;

\$monitor ("[T=%0t a=%0b b=%0b c(and)=%0b d(or)=%0b e(xor)=%0b", \$time, a, b, c, d, e);

for (i = 0; i < 10; i = i+1) begin
#1 	a <= \$random;
b <= \$random;
end
end
endmodule

```
```
Simulation Log
```ncsim> run
[T=0 a=0 b=0 c(and)=0 d(or)=0 e(xor)=0
[T=1 a=0 b=1 c(and)=0 d(or)=1 e(xor)=1
[T=2 a=1 b=1 c(and)=1 d(or)=1 e(xor)=0
[T=4 a=1 b=0 c(and)=0 d(or)=1 e(xor)=1
[T=5 a=1 b=1 c(and)=1 d(or)=1 e(xor)=0
[T=6 a=0 b=1 c(and)=0 d(or)=1 e(xor)=1
[T=7 a=1 b=0 c(and)=0 d(or)=1 e(xor)=1
[T=10 a=1 b=1 c(and)=1 d(or)=1 e(xor)=0
ncsim: *W,RNQUIE: Simulation is complete.

```

## Nand/Nor/Xnor Gates

The inverse of all the above gates are also available in the forms of `nand`, `nor` and `xnor`. The same design from above is reused with the exception that the primitives are switched with their inverse versions.

```  ```

module gates (	input a, b,
output c, d, e);

// Use nand, nor, xnor instead of and, or and xor
// in this example
nand (c, a, b); 	// c is the output, a and b are inputs
nor  (d, a, b);		// d is the output, a and b are inputs
xnor (e, a, b); 	// e is the output, a and b are inputs
endmodule

```
```
```  ```

module tb;
reg a, b;
wire c, d, e;
integer i;

gates u0 ( .a(a), .b(b), .c(c), .d(d), .e(e));

initial begin
{a, b} = 0;

\$monitor ("[T=%0t a=%0b b=%0b c(nand)=%0b d(nor)=%0b e(xnor)=%0b", \$time, a, b, c, d, e);

for (i = 0; i < 10; i = i+1) begin
#1 	a <= \$random;
b <= \$random;
end
end
endmodule

```
```
Simulation Log
```ncsim> run
[T=0 a=0 b=0 c(nand)=1 d(nor)=1 e(xnor)=1
[T=1 a=0 b=1 c(nand)=1 d(nor)=0 e(xnor)=0
[T=2 a=1 b=1 c(nand)=0 d(nor)=0 e(xnor)=1
[T=4 a=1 b=0 c(nand)=1 d(nor)=0 e(xnor)=0
[T=5 a=1 b=1 c(nand)=0 d(nor)=0 e(xnor)=1
[T=6 a=0 b=1 c(nand)=1 d(nor)=0 e(xnor)=0
[T=7 a=1 b=0 c(nand)=1 d(nor)=0 e(xnor)=0
[T=10 a=1 b=1 c(nand)=0 d(nor)=0 e(xnor)=1
ncsim: *W,RNQUIE: Simulation is complete.

```

These gates can have more than two inputs.

```  ```

module gates (	input a, b, c, d,
output x, y, z);

and (x, a, b, c, d); 	// x is the output, a, b, c, d are inputs
or  (y, a, b, c, d);	// y is the output, a, b, c, d are inputs
nor (z, a, b, c, d); 	// z is the output, a, b, c, d are inputs
endmodule

```
```
```  ```

module tb;
reg a, b, c, d;
wire x, y, z;
integer i;

gates u0 ( .a(a), .b(b), .c(c), .d(d), .x(x), .y(y), .z(z));

initial begin
{a, b, c, d} = 0;

\$monitor ("[T=%0t a=%0b b=%0b c=%0b d=%0b x=%0b y=%0b x=%0b", \$time, a, b, c, d, x, y, z);

for (i = 0; i < 10; i = i+1) begin
#1 	a <= \$random;
b <= \$random;
c <= \$random;
d <= \$random;

end
end
endmodule

```
```
Simulation Log
```ncsim> run
[T=0 a=0 b=0 c=0 d=0 x=0 y=0 x=1
[T=1 a=0 b=1 c=1 d=1 x=0 y=1 x=0
[T=2 a=1 b=1 c=1 d=0 x=0 y=1 x=0
[T=3 a=1 b=1 c=0 d=1 x=0 y=1 x=0
[T=4 a=1 b=0 c=1 d=0 x=0 y=1 x=0
[T=5 a=1 b=0 c=1 d=1 x=0 y=1 x=0
[T=6 a=0 b=1 c=0 d=0 x=0 y=1 x=0
[T=7 a=0 b=1 c=0 d=1 x=0 y=1 x=0
[T=8 a=1 b=1 c=1 d=0 x=0 y=1 x=0
[T=9 a=0 b=0 c=0 d=1 x=0 y=1 x=0
[T=10 a=0 b=1 c=1 d=1 x=0 y=1 x=0
ncsim: *W,RNQUIE: Simulation is complete.

```

## Buf/Not Gates

These gates have only one scalar input and one or more outputs. `buf` stands for a buffer and simply transfer the value from input to the output without any change in polarity. `not` stands for an inverter which inverts the polarity of the signal at its input. So a 0 at its input will yield a 1 and vice versa.

```  ```

module gates (	input a,
output c, d);

buf (c, a); 		// c is the output, a is input
not (d, a);		// d is the output, a is input
endmodule

```
```
```  ```

module tb;
reg a;
wire c, d;
integer i;

gates u0 ( .a(a), .c(c), .d(d));

initial begin
a = 0;

\$monitor ("[T=%0t a=%0b c(buf)=%0b d(not)=%0b", \$time, a, c, d);

for (i = 0; i < 10; i = i+1) begin
#1 	a <= \$random;
end
end
endmodule

```
```
Simulation Log
```xcelium> run
[T=0 a=0 c(buf)=0 d(not)=1
[T=2 a=1 c(buf)=1 d(not)=0
[T=8 a=0 c(buf)=0 d(not)=1
[T=9 a=1 c(buf)=1 d(not)=0
xmsim: *W,RNQUIE: Simulation is complete.

```

The last terminal in the port list connects to the input of the gate and all other terminals connect to the output port of the gate. Here is an example of a multiple output buffer, although it is rarely used.

```  ```

module gates (	input  a,
output c, d);

not (c, d, a); 		// c,d is the output, a is input

endmodule

```
```
Simulation Log
```xcelium> run
[T=0 a=0 c=1 d=1
[T=2 a=1 c=0 d=0
[T=8 a=0 c=1 d=1
[T=9 a=1 c=0 d=0
xmsim: *W,RNQUIE: Simulation is complete.

```

## Bufif/Notif

Buffers and Inverters with an additional control signal to enable the output is available through `bufif` and `notif` primitives. These gates have a valid output only if the control signal is enabled else the output will be in high impedance. There are two versions of these, one with normal polarity of control indicated by a 1 like `bufif1` and `notif1` and second with inverted polarity of control indicated by a 0 like `bufif0` and `notif0`.

In the previous session, we built a sequencer, and monitor to work along with the driver. Now, lets put all the three components inside a block called an Agent. Moreover, we'll tweak certain aspects of how they are instantiated and connected within the agent. By doing so, the agent will become re-usable and it'll be easier to just plug it in any environment. Its always the amount of configurability without changing the base code that will determine how re-usable a component is. Also, we'll create a scoreboard that can receive transactions from the monitor.

## Agent

It's better to put the Sequencer, Monitor and Driver inside a uvm component called agent. Usually you'll develop an agent for a particular protocol like USB, AXI, PCIE, etc so that the agent can be plugged into any verification environment and becomes re-usable. To create an agent, simply put all the code inside the `uvm_env` in our previous session, inside a `uvm_agent` block and it's all set. Another feature that we want an agent to have, is the ability to make it passive or active.

A passive agent is one that has only a monitor so that it passively sits by the interface and monitors the transactions. This is useful when there is nothing particular to be driven to the DUT. An active agent is one which has all the three components especially the driver and sequencer, so that data can be sent to the DUT.

In Introduction, we saw that most of the verification components are inherited from `uvm_report_object` and hence they already have functions and methods to display messages.

## Calling Functions

There are four basic reporting functions that can be used with different verbosity levels.

```  ```

uvm_report_* ("TAG", \$sformatf ("[Enter the display message]"), VERBOSITY_LEVEL);

```
```

where * can be either info, error, warning, fatal. UVM has six levels of verbosity with each one represented by an integer.

```  ```

typedef enum {
UVM_NONE    = 0,
UVM_LOW     = 100,
UVM_MEDIUM  = 200,
UVM_HIGH    = 300,
UVM_FULL    = 400,
UVM_DEBUG   = 500
} uvm_verbosity;

```
```

In Data and Driver, we added a data packet and driver, but that model has the following drawbacks:

• Limited verification components
• Data packet is generated inside the Driver
• Protocol used to send data is hard coded in the driver class
• Data packet had a custom display function
Now, we'll add a sequencer and a monitor to the environment. The sequencer will generate, randomize data packets and send it to the driver. The driver will extract necessary information from the data packet and toggle DUT ports via the virtual interface handle. The monitor simply observes the transactions happening across the interface signals and converts it back into data packet format which can be sent to other verification components, if any.

## Data

Previously we used to register a new object class with the factory using ``uvm_object_utils`. If we register it as shown below, we'll be able to utilize the automation features of UVM. That means, we don't have to explicitly write a display function as we did before, or write copy/clone/compare functions for each `uvm_object` inherited class.

```  ```

`uvm_object_utils_begin (my_data)
`uvm_field_int (data, UVM_ALL_ON)
`uvm_object_utils_end

```
```
So, if we want to print out the contents in the class, all we need to do is call the `print ()` method. We can specify the format in which data should be printed - table, line, tree, etc

```  ```

data_obj.print ();
data_obj.print (uvm_default_line_printer);
data_obj.print (uvm_default_tree_printer);
data_obj.print (uvm_default_table_printer);

```
```

## Driver

Since we extended the driver from `uvm_driver` class, it will have handles to the TLM interface. TLM stands for Transaction Level Modeling. For now, all you need to know is that it is a port that can be connected with another TLM interface. You may also refer to TLM tutorial. Usually we connect this with the sequencer, because that's where the data packets are generated. `get_next_item ()` is a task in the TLM object `seq_item_port` which will get an item from the interface. `drive_item` is a user defined, protocol specific task that will do the pin wiggling of the DUT. After data is driven out, the driver will indicate back to the sequencer that it has finished the process by calling the function `item_done`.

## Sequence

We need a sequence for the sequencer to operate on, and a base_sequence can be coded as

```  ```

class base_sequence extends uvm_sequence;
`uvm_object_utils (base_sequence)

my_data  data_obj;
int unsigned      n_times;

function new (string name = "base_sequence");
super.new (name);
endfunction

// Raise an objection if started as the root sequence
if (starting_phase != null)
starting_phase.raise_objection (this);

```
```

`uvm_sequence` is indirectly a derivative of `uvm_object` and hence we have registered it with the factory using ``uvm_object_utils`. Every sequence has `body ()` task which will execute patterns and consume simulation time. You guessed it right, this is where we put our code for a sequence. The `pre_body ()` task will automatically be called (unless it is configured to be skipped) before the body() task and it's a good place to raise an objection to let the testbench know that the sequence has items still pending for execution.

```  ```

`uvm_info ("BASE_SEQ", \$sformatf ("Starting body of %s", this.get_name()), UVM_MEDIUM)
data_obj = my_data::type_id::create ("data_obj");

repeat (n_times) begin
start_item (data_obj);
assert (data_obj.randomize ());
finish_item (data_obj);
end
`uvm_info (get_type_name (), \$sformatf ("Sequence %s is over", this.get_name()), UVM_MEDIUM)

// Drop objection if started as the root sequence
if (starting_phase != null)
starting_phase.drop_objection (this);
endclass
```
```

Within the body() task, we can use `start_item ()` and `finish_item()` methods to send the data object to the driver. Note that finish_item() will be over only after the driver calls `item_done`. After the body() method is over, objection is dropped and simulation proceeds to the next phase.

## Sequencer

The sequencer is NOT required to be derived from `uvm_sequencer`, but instead can be instantiated directly as an object of `uvm_sequencer` parameterized to handle a specific data type. This is done in env class, where driver and monitor are instantiated.

```  ```

uvm_sequencer #(my_data) m_seqr0;

```
```

The important part now is to connect the sequencer with the driver in the `connect_phase ()` method.

```  ```

m_drv0.seq_item_port.connect (m_seqr0.seq_item_export);

```
```

## Simulation Output

```----------------------------------------------------------------
CDNS-UVM-1.1d (14.10-s004)
(C) 2007-2013 Mentor Graphics Corporation
(C) 2007-2013 Cadence Design Systems, Inc.
(C) 2006-2013 Synopsys, Inc.
(C) 2011-2013 Cypress Semiconductor Corp.
----------------------------------------------------------------
UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO @ 0: reporter [UVMTOP] UVM testbench topology:
------------------------------------------------------------
Name                     Type                    Size  Value
------------------------------------------------------------
uvm_test_top             base_test               -     @2629
m_top_env              my_env                  -     @208
m_drv0               my_driver               -     @202
rsp_port           uvm_analysis_port       -     @2828
seq_item_port      uvm_seq_item_pull_port  -     @2777
m_mon0               my_monitor              -     @2728
m_seqr0              uvm_sequencer           -     @2859
rsp_export         uvm_analysis_export     -     @2949
seq_item_export    uvm_seq_item_pull_imp   -     @3497
arbitration_queue  array                   0     -
lock_queue         array                   0     -
num_last_reqs      integral                32    'd1
num_last_rsps      integral                32    'd1
------------------------------------------------------------

UVM_INFO ./tb/my_pkg.sv(88) @ 0: uvm_test_top.m_top_env.m_drv0 [my_driver] Applying initial reset
UVM_INFO ./tb/my_pkg.sv(92) @ 390000: uvm_test_top.m_top_env.m_drv0 [my_driver] DUT is now out of reset
UVM_INFO ./tb/my_pkg.sv(99) @ 390000: uvm_test_top.m_top_env.m_drv0 [my_driver] Waiting for data from sequencer
UVM_INFO ./tb/my_pkg.sv(186) @ 390000: uvm_test_top.m_top_e[email protected]@base_sequence [BASE_SEQ] Starting body of base_sequence
UVM_INFO ./tb/my_pkg.sv(194) @ 390000: [email protected]@base_sequence [base_sequence] Sequence base_sequence is over
UVM_INFO ./tb/my_pkg.sv(118) @ 390000: uvm_test_top.m_top_env.m_drv0 [my_driver] Finished DUT simulation

--- UVM Report catcher Summary ---

Number of demoted UVM_FATAL reports  :    0
Number of demoted UVM_ERROR reports  :    0
Number of demoted UVM_WARNING reports:    0
Number of caught UVM_FATAL reports   :    0
Number of caught UVM_ERROR reports   :    0
Number of caught UVM_WARNING reports :    0

--- UVM Report Summary ---

** Report counts by severity
UVM_INFO :    8
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[BASE_SEQ]     1
[RNTST]     1
[UVMTOP]     1
[base_sequence]     1
[my_driver]     4
Simulation complete via \$finish(1) at time 390 NS + 139

```

Go to the final step Agent and Scoreboard

In the previous article, we saw how a UVM driver gets the next item by the calling `get_next_item` method, and how it informs the sequencer that the current item is done. Although this is the preferred way for driver-sequencer communications, UVM also gives us an alternative for a more complex implementation.

The other way is for the driver to use the `get` method to receive the next item and later use `put` to give a response item back to the sequencer.

## What is the driver code for `get` and `put` calls ?

```  ```

class my_driver extends uvm_driver #(my_data);
`uvm_component_utils (my_driver)

super.run_phase(phase);

// 1. Get an item from the sequencer using "get" method
seq_item_port.get(req);

// 2. For simplicity, lets assume the driver drives the item and consumes 20ns of simulation time
#20;

// 3. After the driver is done, assume it gets back a read data called 8'hAA from the DUT
// Assign the read data into the "request" sequence_item object
req.data = 8'hAA;

// 4. Call the "put" method to send the request item back to the sequencer
seq_item_port.put(req);
endclass

```
```

## How does the sequence start and stop an item ?

```  ```

class my_sequence extends uvm_sequence #(my_data);
`uvm_object_utils (my_sequence)

// Create a sequence item object handle to store the sequence_item contents
my_data tx;

// 1. Create the sequence item using standard factory calls
tx = my_data::type_id::create("tx");

// 2. Start this item on the current sequencer
start_item(tx);

// 3. Do late randomization since the class handle pointers are the same
tx.randomize();

// 4. Finish executing the item from the sequence perspective
// The driver could still be actively driving and waiting for response
finish_item(tx);

// 5. Because "finish_item" does not indicate that the driver has finished driving the item,
// the sequence has to wait until the driver explicitly tells the sequencer that the item is over
// So, wait unitl the sequence gets a response back from the sequencer.
get_response(tx);

`uvm_info ("SEQ", \$sformatf("get_response() fn call done rsp addr=0x%0h data=0x%0h, exit seq", tx.addr, tx.data), UVM_MEDIUM)
endclass

```
```

## Example

To illustrate how the `get` and `put` method calls between driver and sequencer work, let us build a simple testbench structure like the one shown below.

### Define a sequence item

To keep things simple, let us define a transaction object class that will become the sequence item used for sequencer-driver communication. Assume that this sequence item contains two variables called addr and data.

```  ```

// Note that this class is dervide from "uvm_sequence_item"
class my_data extends uvm_sequence_item;
rand bit [7:0]   data;

// Rest of the class contents come here ...
endclass

```
```

### Define the driver

The `uvm_driver` is parameterized to accept a class object of the type my_data and the driver is expected to unpack this class object and drive the signals appropriately to the DUT via the interface.

```  ```

class my_driver extends uvm_driver #(my_data);
`uvm_component_utils (my_driver)

super.run_phase(phase);

// 1. Get sequence item from the sequencer using "get" method.
// "req" is a pre-defined variable that comes with the class "uvm_driver"
`uvm_info ("DRIVER", \$sformatf ("Waiting for data from sequencer"), UVM_MEDIUM)
seq_item_port.get(req);

// 2. Assume that the driver drives this data during this time. For our purpose let's consider that the driver
// consumes 20ns of simulation time
#20;

// 3. Lets also assume that the DUT returned some read data back to the driver, which needs to be sent back
// to the sequence. Note that read data is put into the same "request" class object handle
`uvm_info ("DRIVER", \$sformatf ("#20 delay over, curr data=0x%0h", req.data), UVM_MEDIUM)
req.data = 8'hAA;

// 4. The driver calls the "put" method and sends back the response data to the sequencer
`uvm_info ("DRIVER", \$sformatf ("About to call put() with new data=0x%0h", req.data), UVM_MEDIUM)
seq_item_port.put(req);
endclass

```
```

### Define the sequence

A sequence item is always started using the `start_item` and `finish_item` methods.

```  ```

class my_sequence extends uvm_sequence #(my_data);
// Rest of the sequence code

// 1. Create a sequence item of the given type. Note that this sequence has an internal
// variable called "req" which can be directly used as well instead of "tx"
my_data tx = my_data::type_id::create("tx");
`uvm_info ("SEQ", \$sformatf("About to call start_item"), UVM_MEDIUM)

// 2. Start the item on the sequencer which will send this to the driver
start_item(tx);
`uvm_info ("SEQ", \$sformatf("start_item() fn call done"), UVM_MEDIUM)

// 3. Do some late randomization to create a different content in this transaction object
tx.randomize();

// 4. Call finish_item to let driver continue driving the transaction object or sequence item
finish_item(tx);
`uvm_info ("SEQ", \$sformatf("finish_item() fn call done, wait for rsp"), UVM_MEDIUM)

// 5. Wait for the driver to finish driving the object, and respond back with a response object
// The transaction is said to be complete at this point. This task is blocking in nature and hence
// the sequence will end only after the driver returns a response item.
get_response(tx);
`uvm_info ("SEQ", \$sformatf("get_response() fn call done rsp addr=0x%0h data=0x%0h, exit seq", tx.addr, tx.data), UVM_MEDIUM)
endclass

```
```

### Define the test class

To keep things simple, let us directly create instances of the driver and sequencer inside the test class.

Driver and Sequencer should be instantiated inside an agent. An agent is instantiated in an environment and the the environment in turn should be created in the test.

```  ```

class base_test extends uvm_test;
// Rest of the test code is here

// The sequencer is parameterized to accept objects of type "my_data" only
my_driver                	m_drv0;
uvm_sequencer #(my_data) 	m_seqr0;
my_sequence   				m_seq;

// Build the sequencer and driver components
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_drv0 = my_driver::type_id::create ("m_drv0", this);
m_seqr0 = uvm_sequencer#(my_data)::type_id::create ("m_seqr0", this);
endfunction

// Connect the sequencer "export" to the driver's "port"
virtual function void connect_phase (uvm_phase phase);
super.connect_phase (phase);
m_drv0.seq_item_port.connect (m_seqr0.seq_item_export);
endfunction

// Start the sequence on the given sequencer
m_seq = my_sequence::type_id::create("m_seq");
phase.raise_objection(this);
m_seq.start(m_seqr0);
phase.drop_objection(this);
endclass

```
```

Note that `finish_item` finished at time 0ns even before the driver started doing the pin wiggling (assumed to happen during the 20ns delay in this example). The `get_response` method is done at time 20ns when the driver sends back the response item.

It is also evident from the log that the data modified in the request packet within the driver can be obtained inside the sequence.

Simulation Log
```UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO testbench.sv(33) @ 0: uvm_test_top.m_top_env.m_drv0 [DRIVER] Waiting for data from sequencer
UVM_INFO testbench.sv(76) @ 0: [email protected]@m_seq [SEQ] About to call start_item
UVM_INFO testbench.sv(78) @ 0: [email protected]@m_seq [SEQ] start_item() fn call done
UVM_INFO testbench.sv(80) @ 0: [email protected]@m_seq [SEQ] tx randomized with addr=0x8f data=0x1d
UVM_INFO testbench.sv(35) @ 0: uvm_test_top.m_top_env.m_drv0 [DRIVER] Start driving tx addr=0x8f data=0x1d
UVM_INFO testbench.sv(82) @ 0: [email protected]@m_seq [SEQ] finish_item() fn call done, wait for rsp
UVM_INFO testbench.sv(37) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] #20 delay over, curr data=0x1d
UVM_INFO testbench.sv(39) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] About to call put() with new data=0xaa
UVM_INFO testbench.sv(41) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] Finish driving tx addr=0x8f data=0xaa
UVM_INFO testbench.sv(84) @ 20: [email protected]@m_seq [SEQ] get_response() fn call done rsp addr=0x8f data=0xaa, exit seq
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 20: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 20: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---

```