Cuando se utilizan asignaciones procedurales en un diseño RTL con Verilog, es posible controlar el instante en el que ocurren mediante eventos, los cuales pueden ser de nivel o de flanco. Los eventos de flanco permiten modificar el estado de las variables durante un flanco ascendente o descendente de reloj. Sin embargo, el tipo de asignación durante un evento puede modificar significativamente el diseño de un circuito. Este artículo demuestra su diferencia con un ejemplo práctico.



Verilog soporta tres niveles de abstracción para el diseño de circuitos digitales:

  • Nivel de compuerta: corresponde a una descripción de bajo nivel del diseño. Se conoce como "modelo estructural" y se describe mediante primitivas lógicas (and, or, not, etc.); conexiones lógicas y propiedades de tiempo. Se utilizan señales discretas que pueden valer 0, 1, X (indefinido) y Z (alta impedancia).
  • Nivel de transferencia de registro (RTL): se especifican las características de un circuito mediante operaciones de transferencia de datos entre registros. Es el único nivel de diseño sintetizable.
  • Nivel de comportamiento: se independiza de la estructura del diseño y simplemente define el comportamiento del mismo mediante algoritmos en paralelo conformados por instrucciones que se ejecutan de forma secuencial.

En los diseños RTL, Verilog permite controlar el momento en el que ocurre una asignación procedural dentro de un proceso mediante temporizaciones (retrasos o delays) o eventos (cambio en el valor de una variable). A su vez, los eventos se dividen en dos tipos: de nivel y de flanco.

Los eventos de nivel se producen cuando cambia el valor de una variable, mientras que los eventos de flanco se producen durante un flanco de subida o bajada en una variable.

Los eventos de flanco son utilizados típicamente en diseños de circuitos secuenciales sincrónicos, donde se desea que los valores se modifiquen únicamente durante un flanco de reloj (ascendente o descendente). Sin embargo, el tipo de asignación puede afectar notablemente el comportamiento del circuito.

Existen dos tipos de asignaciones en Verilog: bloqueantes (=) y no bloqueantes (<=). Las asignaciones bloqueantes provocan la ejecución secuencial de un bloque de instrucciones, mientras que las asignaciones no bloqueantes hacen que las instrucciones de un mismo bloque se ejecuten en paralelo.

Existe una regla popular que dice que se deben utilizar asignaciones bloqueantes para bloques que generen lógica combinacional, mientras que se deben utilizar asignaciones no bloqueantes para bloques que generen lógica secuencial. Veamos con un simple ejemplo el por qué de esta regla.

El siguiente diseño presenta un pequeño módulo secuencial "xy" que contiene dos registros de 4 bits inicializados en "0001". Se desea que en cada tic de reloj se haga un shift a izquierda del lsb (bit menos significativo), ingresando siempre 1 en el mismo.

Para diferenciar el comportamiento de cada tipo de asignación, el registro x se modificará con asignaciones bloqueantes y el y con no bloqueantes:


module xy(clk);
    input clk;
    reg [3:0]x;
    reg [3:0]y;

    // Inicializar x
    initial
    begin
        x=1;
        y=1;
    end

    // Cambiar en el flanco ascendente del reloj
    always @(posedge clk)
    begin
        // Shift del lsb de x (nivel)
        x[1] = x[0];
        x[2] = x[1];
        x[3] = x[2];

        // Shift del lsb de y (flanco)
        y[1] <= y[0];
        y[2] <= y[1];
        y[3] <= y[2];
    end

endmodule

module tester(clk);
    output clk;
    reg clk;

    initial
    begin
        $dumpfile("x.vcd");
        $dumpvars;
        clk=0; #10 $finish;
    end

    always
    begin
        #1 clk = !clk;
    end

endmodule

module testbench;
    wire clk;
    xy xy1(clk);
    tester t(clk);
endmodule

Al compilar, simular y graficar con GTKWave se obtiene la siguiente gráfica de señales:

Luego del primer tic de reloj (registro clk) se modifican todos los bits de x (todos los shifts ocurren en un mismo tic). En cambio, los bits de y se modifican uno a uno en cada tic de reloj. Este último era el comportamiento deseado para el módulo.

¿Por qué se modifican todos los bits de x en un mismo flanco (tic de reloj)? Porque al utilizar asignaciones bloqueantes, cada instrucción ocurre luego de que se haya completado la anterior, en orden secuencial. Como resultado el bloque de instrucciones que modifican los bits de x tienen un comportamiento combinacional. En un mismo flanco se transfiere el 1 en el lsb por todo el registro:


        x = 0 0 0 1

(clk)   x = 1 1 1 1

Al ser bloqueantes, cada shift ocurre luego de se haya actualizado el bit anterior:


        // x=0001
        x[1] = x[0]; // ahora x[1]=1, x=0011
        // x=0011
        x[2] = x[1]; // ahora x[2]=1, x=0111
        // x=0111
        x[3] = x[2]; // ahora x[3]=1, x=1111

En las asignaciones no bloqueantes en cambio todas parten con el mismo valor de y en paralelo:

        // y=0001
        y[1] <= y[0]; // ahora y[1]=1, y=0011
        // y=0001
        y[2] <= y[1]; // ahora y[2]=0, y=0011
        // y=0001
        y[3] <= y[2]; // ahora y[3]=0, y=0011

En el siguiente tic de reloj y=0111 y luego y=1111. Para más información recomiendo leer el paper en las referencias.


        y = 0 0 0 1

(clk)   y = 0 0 1 1

(clk)   y = 0 1 1 1

(clk)   y = 1 1 1 1

Referencias


Tal vez pueda interesarte


Compartí este artículo