01signal.com

Implementación de FIFO de reloj único en Verilog

Alcance

Esta página es la tercera de una serie de cinco páginas dedicadas a las FIFO y muestra una implementación en Verilog de una FIFO básica de reloj único. Esto puede resultar útil para escribir código portátil, pero el objetivo principal de esta página es insistir en cómo funciona una FIFO. Así que mostraré una implementación de una «FIFO estándar» y también de una FIFO FWFT. Pero antes necesitamos una RAM de doble puerto (dual port RAM) que usarán ambas FIFO.

La RAM de doble puerto

Este es el módulo Verilog que implementa la RAM mediante inferencia. Es muy probable que cualquier sintetizador (synthesizer) lo haga bien, pero es posible que genere el tipo de RAM no deseado (RAM de bloques (block RAM) frente a RAM distribuida (distributed RAM)), así que puede ser necesario añadir directivas para el sintetizador. O quizá usar un módulo de IP (IP core) suministrado por el fabricante de la FPGA para una RAM de doble puerto, si es lo que mejor funciona.

Así que este es el módulo:

module dualport_ram #(parameter depth = 64,
                      log2_depth = 6,
                      width = 8
                     )
   (
    input                    clk,
    input [(log2_depth-1):0] wr_addr,
    input [(log2_depth-1):0] rd_addr,
    output reg [(width-1):0] rd_data,
    input [(width-1):0]      wr_data,
    input                    rd_en,
    input                    wr_en
    );

   reg [(width-1):0] inferred_ram[0:(depth-1)];

   always @(posedge clk)
     begin
        if (wr_en)
          inferred_ram[wr_addr] <= wr_data;

        if (rd_en)
          rd_data <= inferred_ram[rd_addr];
     end
endmodule

La «FIFO estándar»

Y ahora estamos listos para ver el módulo que implementa una «FIFO estándar» (es decir, no una FIFO FWFT):

module fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   wire                       fetch_data, commit_data;

   assign fetch_data = rd_en && !empty;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

La única nota de uso importante está en el comentario de arriba, en relación con los parámetros «depth» y «log2_depth»: depth debe ser igual a 2log2_depth.

El funcionamiento de este módulo es bastante sencillo: @fetch_data es como @rd_en, pero teniendo en cuenta @empty (vacía). @fetch_data es, por tanto, una versión segura de @rd_en, en el sentido de que aunque @rd_en y @empty estén en alto al mismo tiempo (lo cual es ilegal), no ocurre nada malo, porque en ese caso se ignora @rd_en.

@commit_data es la versión segura de @wr_en de la misma manera, teniendo en cuenta @full (llena).

El siguiente valor de @words_in_ram, @next_words_in_ram, se calcula como una función combinacional, a partir de @fetch_data y @commit_data (observa la instrucción always @(*)). @next_words_in_ram se utiliza para generar los valores de varios registros con este fragmento de código:

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);
[ ... ]

Observa que aquí se definen @full (llena) y @empty (vacía).

Lo que hace que esta FIFO sea tan fácil de implementar, en comparación con una FIFO de doble reloj, es la posibilidad de definir @words_in_ram de esta manera y usar ese mismo registro en ambos lados de la FIFO.

Siguiendo con el código, tenemos la actualización de @rd_addr y @wr_addr, y después la cláusula para @rst. Observa que tanto @empty (vacía) como @full (llena) pasan a alto como consecuencia del reset, pero @full volverá a bajo cuando el reset se desactive.

Una nota sobre el estilo de código: cuando @rst está en alto, las asignaciones del bloque begin-end de la instrucción if anulan las asignaciones que se hubieran hecho antes, así que @rst realmente resetea todos los registros a sus valores iniciales. Este no es el estilo de código más habitual, pero tiene ventajas evidentes cuando no todos los registros se resetean. Este caso concreto no demuestra esta ventaja, pero puedes verlo en esta página.

Por último, se incluye una instanciación (instantiation) de la RAM de doble puerto.

La FIFO FWFT

Como se muestra en esta página, es bastante fácil convertir una «FIFO estándar» en una FIFO FWFT. Pero su implementación directa permite comentar algunos puntos interesantes, así que aquí está:

module fwft_fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   reg                        has_more_words;
   wire                       fetch_data, commit_data;

   assign fetch_data = (rd_en || empty) && has_more_words;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        has_more_words <= (next_words_in_ram != 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (fetch_data)
          empty <= 0;
        else if (rd_en)
          empty <= 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             has_more_words <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

En primer lugar, observa que tenemos un registro nuevo: @has_more_words. Compara su definición con la «FIFO estándar» de arriba y convéncete de que @has_more_words es la negación lógica de @empty (vacía).

A continuación, observa que la definición de @fetch_data ha cambiado. Ahora está protegida por @has_more_words en lugar de por @empty (vacía), como era de esperar, de modo que dice:

assign fetch_data = (rd_en || empty) && has_more_words;

Recuerda de antes que @empty (vacía) significa «@dout no es válido» en una FIFO FWFT. Esta asignación significa que, además de cuando @rd_en está activo, si la salida no es válida y hay datos en la memoria de los que leer, se hace esa lectura. Eso es, exactamente, el 'fall through' de la primera palabra.

Y, por último, la lógica para asignar @empty (vacía) ha cambiado a:

   always @(posedge clk)
     if (fetch_data)
       empty <= 0;
     else if (rd_en)
       empty <= 1;

Esto viene a decir sencillamente que, si se lee una palabra de la memoria, @empty (vacía) pasa a bajo en el siguiente ciclo de reloj, porque ahora, evidentemente, hay un dato nuevo y válido. En cambio, si eso no ocurre y @rd_en está activo de todos modos, la lógica de la aplicación acaba de tomar la última palabra disponible, así que @empty debe pasar a alto. Observa que si @rd_en está en alto y @fetch_data está en bajo, @has_more_words sin duda está en bajo (mira la asignación de @fetch_data más arriba). Por eso esta condición equivale a leer la última palabra de la FIFO.

Con esto concluye la tercera página de esta serie sobre las FIFO. La siguiente página muestra cómo adaptar una «FIFO estándar» para usarla en una aplicación de adquisición de datos (data acquisition).

Esta página se ha traducido del inglés mediante traducción automática. En caso de duda, consulta el texto original.
Copyright © 2021-2026. All rights reserved. (dcc38493)