The uvm_object class is the base class for all UVM classes. From it, all the rest of classes are extended. It provides basic functionalities such as print, compare, copy and similar methods.

This class can be used when defining reusable parts of a sequence items. For example, in a packet like uvm_sequence_item, we could define a uvm_object extended object for defining the header. This would be:

class packet_header extends uvm_object;

   rand bit [2:0] len;
   rand bit [2:0] addr;

      `uvm_field_int(len, UVM_DEFAULT)
      `uvm_field_int(addr, UVM_DEFAULT)

   function new (string name="packet_header");;
   endfunction : new

endclass : packet_header

This packet_header could be included in a packet class for conforming the uvm_sequence_item (the transaction) which will compose the sequences:

class simple_packet extends uvm_sequence_item;

   rand packet_header header;
   rand bit [4:0] payload;

      `uvm_field_object(header, UVM_DEFAULT)
      `uvm_field_int(payload, UVM_DEFAULT)

   function new (string name = "simple_packet");;
      header = packet_header::type_id::create("header");
   endfunction : new

endclass : packet


Let’s z be a 2D point in the space as \(z = x + jy\), if we want to rotate this point a given angle \(\theta\), we get the following expressions:
\[e^{j\theta} \cdot z = \left(\cos{\theta} + j \sin{\theta}\right)\left(x+jy\right) \\ = x\cos{\theta}-y\sin{\theta} + j \left(y \cos{\theta} + x \sin{\theta} \right) \\ = x’ + j y’ \]

Then, for a generic point, the rotation can be expressed as an equation system, where \(x’\) and \(y’\) are the new coordinates, \(\theta\) is the rotation angle and \(x\) and \(y\) are the original coordinates:
\cos{\theta} & -\sin{\theta}\\
\sin{\theta} & \cos{\theta}
\end{bmatrix} \]

This rotation can be coded in MATLAB as:

%% Function to rotate vector
function v = rotate(P, theta)
    rot = [cos(theta) -sin(theta);
           sin(theta) cos(theta)];
    v = rot*P;

A possible implementation of the cordic algorithm could be:

%% Clear all previous values
clear all;

%% Define vectors
A = [2;-3];
O = [0;0];

%% Define accuracy
error_limit = 0.01;

%% Initialize variables
start_angle = pi/2; % 90º
current_angle = start_angle;
acc_angle = 0;

A_rotated = A;
steps = 0;

% Second quadrant (90º - 180º)
if(A_rotated(1) < 0 && A_rotated(2) > 0)
    A_rotated = rotate(A_rotated, -pi/2);
    acc_angle = pi/2;
% Third quadrant (180º - 270º)
elseif(A_rotated(1) < 0 && A_rotated(2) < 0)
    A_rotated = rotate(A_rotated, -pi);
    acc_angle = pi;
% Forth quadrant (270º - 360º)
elseif(A_rotated(1) > 0 && A_rotated(2) < 0)
    A_rotated = rotate(A_rotated, -3*pi/2);
    acc_angle = 3*pi/2;    

%% Compute angle
% Keep rotating while error is too high
while(abs(A_rotated(2)) > error_limit)
    % Represent current vector
    quiver(0, 0,A_rotated(1), A_rotated(2));
    % Keep previous vectors
    hold on;
    % Decrease angle rotation
    current_angle = current_angle/2;
    % (For debugging purposes)
    current_angle_deg = current_angle*180/pi;
    % Save current error
    error = A_rotated(2);
    % If y coordinate is still positive
    if(error > 0)
        % Rotate again conterclockwise
        A_rotated = rotate(A_rotated, -1*current_angle);
        % Accumulate rotated angle
        acc_angle = acc_angle + current_angle;
    % If y coordinate is negative
        % Rotate vector clockwise
        A_rotated = rotate(A_rotated, current_angle);
        % Substract current angle to the accumulator because we have
        % overcome the actual angle value
        acc_angle = acc_angle - current_angle;
    % (For debugging purposes)
    acc_angle_deg = acc_angle*180/pi;
    % Increase step counter
    steps = steps + 1;

% Print angle, hypotenuse length and number of steps needed to compute
fprintf('Angle = %f\nHypotenuse = %f\nNumber of steps: %d\n', acc_angle*180/pi, A_rotated(1), steps);

%% Function to rotate vector
function v = rotate(P, theta)
    rot = [cos(theta) -sin(theta);
           sin(theta) cos(theta)];
    v = rot*P;


I have coded an interactive applet to illustrate the algorithm. It has been done using the p5.js library. The error limit has been set to \(0.5\).

#define round(x) x >= 0.0 ? (int)(x + 0.5) : ((x - (double)(int)x) >= -0.5 ? (int)x : (int)(x - 0.5))


#include <stdio.h>

#define round(x) x >= 0.0 ? (int)(x + 0.5) : ((x - (double)(int)x) >= -0.5 ? (int)x : (int)(x - 0.5))

void main(void){
   float f1 = 3.14, f2 = 6.5, f3 = 7.99;
   int r1, r2, r3;
   r1 = round(f1);
   r2 = round(f2);
   r3 = round(f3);

   printf("r1 = %d\nr2 = %d\nr3 = %d\n", r1, r2, r3);

The console output is:

r1 = 3
r2 = 7
r3 = 8

UVM introduces the concept of phases to ensure that all objects are properly configured and connected before starting the runtime simulation. Phases contribute to a better synchronised simulation and enable to the verification engineer to get better modularity of the testbench.

UVM phases consists of:

  1. build
  2. connect
  3. end_of_elaboration
  4. start_of_simulation
  5. run
    1. reset
    2. configure
    3. main
    4. shutdown
  6. extract
  7. check
  8. report
  9. final

The run phase has been simplified to get a better picture of how phases worked. Nevertheless, all subphases in the run phase have a pre_ and post_ phase to add flexibility. Therefore, the run phase is actually composed by the following phases:

  1. run
    1. pre_reset
    2. reset
    3. post_reset
    4. pre_configure
    5. configure
    6. post_configure
    7. pre_main
    8. main
    9. post_main
    10. pre_shutdown
    11. shutdown
    12. post_shutdown

Although all phases play an important role, the most relevant phases are:

  • build_phase: objects are created
  • connect_phase: interconnection between objects are hooked
  • run_phase: the test starts. The run_phase is the only phase which is a task instead of a function, and therefore is the only one that can consume time in the simulation.

UVM phases are executed from a hierarchical point of view from top to down fashion. This means that the first object that executes a phase is the top object, usually

testbench  test  environment agent {monitor, driver, sequencer, etc}

Nevertheless, in the connect phase, this happens the other way round in a down to top fashion.

{monitor, driver, sequencer} agent environment test testbench

To use UVM in your Verilog test bench, you need to compile the UVM package top. To do so, you need to include it on your file by using:

`include "uvm_macros.svh"
`include ""
import uvm_pkg::*;

The uvm_pkg is contained in the that must be passed to the compiler. Therefore, it is necessary to indicate the UVM path to the compiler. In Cadence Incisive Enterprise Simulator (IES) is as easy as to specify -uvm switch.

In Modelsim, from Modelsim console, run:

vsim -work work +incdir+/path/to/uvm-1.1d/src +define+UVM_CMDLINE_NO_DPI +define+UVM_REGEX_NO_DPI +define+UVM_NO_DPI

After compilation, click on Simulate > Start simulation and select the tb in the work library. Then, run the simulation for the desired time.

When an operation such as an addtion or a substraction is done using different size operands than final variable, it is necessary to extend sign to ensure the operation is done properly.


logic signed [21:0] acc;
logic signed [5:0] data_in;
logic [3:0] offset;


acc = data_in + offset;

Sign on data_in will not be respected. data_in will be filled with 0 before doing the operation and won’t be taken as negative (if applies).


acc = {{16{data_in[5]}},data_in} + offset;

Extend sign to match number of acc_add bits before doing operation

Máxima frecuencia de trabajo

La frecuencia máxima de trabajo de un diseño digital, viene marcado por los tiempos de flip-flop, el tiempo de propagación en la lógica combinacional y el tiempo de setup. Para registar un dato a la salida de un flip-flop, es necesario un tiempo para pasar el nivel de entrada D1 a la salida Q1, lo que se conoce como tiempo de propagación de flip-flop. Luego, esta señal pasará a través de una lógica combinacional que añadirá un retardo. Este retardo en la figura anterior es \(t_{pLCR}\). Finalmente, para que último flip-flop pueda registrar el nivel de entrada, es necesario que este dato se mantenga al menos un tiempo de setup \(t_{su}\). Por tanto, el periodo mínimo de el ciclo de reloj debe ser:

\[ T_{p~min} = t_{pFF} + t_{pLCR} + t_{su} \]

Y la frecuencia máxima, será la inversa del periodo mínimo:

\[f_{max} = \frac{1}{T_{p~min}} \]

Los tiempo de setup y de flip-flop vienen determinados por la tecnología con la que se ha fabricado la FPGA. Por lo tanto, estos dos parámetros quedan invariables en un diseño. Para poder aumentar la frecuencia de trabajo, es necesario disminuir el tiempo de retardo que se produce en la lógica combinacional. Para ello, es necesario añadir registros en medio de la lógica combinacional que hagan que el retardo máximo entre flip-flops se minimice. Esto se conoce como pipelining.

Sin embargo, al añadir registros, el número de ciclos que tardará en salir el primer dato a la salida del sistema aumentará. El número de ciclos que tarda el primer dato en salir se conoce como latencia.

Para obtener el número negativo de un número en codificación Ca2, hay que negar todos los bit y sumar 1.

Cast de signed y unsigned

Para realizar la conversión números signed y unsigned en Verilog, existe la macro $signed() y $unsigned()

La macro $signed() extiende el bit de signo, de manera que si el valor de la variable unsigned tenía el MSB a 1, al hacer la conversión mediante la macro se obtendrá un valor negativo, machacando el valor original que tenía en formato unsigned.

A U[3,0] = 111 = 7
$signed(A) = -1

Sin embargo, si el número unsigned tenía el MSB a 0, el valor correspondiente después de la conversión será el mismo.

Cast a signed. Si el valor del MSB de uA está a 1, el valor de sA se modificará

Cuando se quiere pasar un número en formato signed a unsigned utilizando la macro $unsigned(), se hace una copia bit a bit del valor signed. Si son necesarios más bits porque la variable donde se copiará el valor posee más bits, se rellenará con ceros.

Aumentando el rango pero mateniendo el mismo tipo

Cuando se quiere aumentar el rango de una variable manteniendo el mismo formato de los números, Verilog sintetiza de manera diferente en función del formato original. En un formato signed, se hace una copia del bit de signo en todos los bits añadidos. En el caso de un formato unsigned, simplemente añade ceros.

Extensión de rango en formato signed
Extensión de rango en formato unsigned

Reduciendo el rango del número

Cuando se reduce el rango del número, se puede proceder de dos maneras. Una es con la técnica wrap, que consiste en no hacer nada. Los bits que ya no caben en el nuevo formato reducido, permanecen desconectados.

La otra manera, es utilizando el overflow. Con overflow, se consigue si al reducir el rango de un número, este supera el rango máximo representable, el valor definitivo queda como el máximo que se puede representar. Este técnica necesita de bloques adicionales para detectar si el número a reducir es mayor que el nuevo rango reducido y asignarle el máximo valor representable.

Bloques para detectar el overflow

Si el formato es con signo, la saturación puede ser positiva o negativa, por tanto hay que controlar ambos casos.

assign As = (A > SATpos) ? SATpos : (A<SATneg) ? SATneg : A[7:0];

Cambiando el ancho de palabra

Reduciendo la precisión con truncado

Para reducir la precisión de un formato, es decir, pasar de A[8,7] a B[5,4], por ejemplo, cuyo rango es [0,1] para ambos pero la precisión en A es de 0.0078125 y en B es de 0.0625, hay que coger los bits que correspondan.

En Verilog esto equivale a:

wire [7:0] A;
wire [4:0] B;

assign B = A[7:3];

Reduciendo la precisión con redondeo

Si hacemos la misma operación pero utilizando el redondeo, hay que tener en cuenta el bit anterior al menos significativo de los que estamos cogiendo para la nueva variable. De esta manera, si está 1, se redondeará hacia arriba y si está a 0 se redondeará hacia abajo.

wire [7:0] A;
wire [4:0] B;

assign B = A[7:3] + A[2];

Aumentando la precisión (o disminuyendo el escalado)

Cuando se añaden bits extras extremo de los LSB se aumenta la precisión del formato. Sin embargo, hay que posicionar correctamente los bits para que tengan el peso adecuado. A nivel de implementación consiste en conector todos los bits a la parte alta de la nueva variable.

En Verilog hay dos maneras de hacerlo. Asignar todos los bits de A a la parte alta de B y añadir 0 por abajo:

wire [4:0] A;
wire [7:0] B;

assign B = {A, 3'b0};

O asignando A a B desplazado 3 bits, ya que al mover rellenará con ceros.

wire [4:0] A;
wire [7:0] B;

assign B = A << 3;

Desplazamiento por una constante

Cuando se desplaza una variable por una constante, existen dos operadores para realizar el desplazamiento: >> y >>> (y sus equivalentes para la otra dirección de desplazamiento).

>> corresponde a un desplazamiento lógico, en el que no se tiene en cuenta si el MSB de la variable corresponden al bit de signo. Por tanto, al desplazar hacia la derecha pondrá ceros en la parte alta de la variable.

>>> corresponde a un desplazamiento aritmético, en el que sí se tiene en cuenta que el MSB es el bit de signo, de manera que al desplazar hacia la derecha hará una copia del signo. Por tanto, cada vez que se esté utilizando una variable signed, será necesario utilizar el desplazamiento aritmético (<<</>>>) para mantener el valor del signo.

Hay que tener en cuenta que si la variable destino a la que estamos asignando el desplazamiento tiene el mismo tamaño que la variable origen, se van a perder los bits que desplacemos. Por tanto, si queremos mantener esos bits que se desplazan la variable destino debe ser n bits mayor, donde n es el número de bits desplazados.

Las FPGAs como Altera Cyclone IV llevan incorporados arrays de bloques lógicos (LAB). Estos LABs en su interior están formados por Logic Elements (LE) que a su vez incorporan una LUT, un seguidor de acarreo y un registro.

Estos elementos son los componentes básicos para realizar operaciones de suma y multiplicación de una FPGA.


Suma manteniendo precisión

Al sumar manteniendo la precisión, es suficiente con alinear correctamente los bits y que la variable de destino tenga bits suficientes para alojar el resultado.

Suma recortando precisión

Al recortar la precisión, no hay que sumar todos los bits de los operandos. Solo hay que seleccionar los que se vayan a incorporar a la variable destino.


La resta (A-B) equivale a realizar un suma de A y con la inversión mediante Ca2 de B.  Para implementarla no es necesario añadir un sumador más y multiplexar la salida en función del signo. Simplemente hay que negar B y sumarlo con A.


Para generar señales digitales periódicas, como por ejemplo senos o cosenos, se suele utiliza la técnica de síntesis digital directa. Esta consiste en almacenar la señal en una memoria y recorrer las diferentes direcciones de la memoria para obtener la señal. Por tanto, se necesita de un contador que incremente un determinado número de pasos la dirección de memoria a la que hay que acceder.

La frecuencia sintetizada depende de 3 factores:

  • P: el incremento que se hace de la dirección de memoria
  • \(f_{clk}\): la frecuencia de reloj
  • \(2^M\): el número de muestras que hay de un periodo de la señal.

Ejemplo: síntesis de un seno de 1 Hz

Tenemos almacenado en un memoria el ciclo completo de un seno como aparece en la imagen utilizando 16 muestras (\(2^M = 16\)) :

La frecuencia del reloj es de 8 Hz, es decir, saca 8 muestras por segundo. Por tanto, para obtener una frecuencia del seno de salida de 1 Hz, debemos sacar 8 muestras. Esto conduce a la conclusión de que debemos obtener las muestras de la memoria de 2 en 2. Lo que significa que P = 2.

Por tanto,

\[ f_{sintetizada} = \frac{P f_{clk}}{2^M} \]

El máximo paso que se puede dar es \(P = 2^{M-1}\) para poder cumplir el teorema de Nyquist y tener una señal parecida a la de la imagen:

En este caso, la frecuencia es máxima y corresponde a \(\frac{f_{clk}}{2}\). El mínimo paso que se puede dar es P= 1, en el que la frecuencia es mínima y corresponde a \(\frac{f_{clk}}{2^M}\).