Esta página es la primera de una serie de cinco páginas dedicadas a las FIFO.
Visión general
La FIFO FPGA es un elemento de memoria con un concepto muy sencillo: una parte de la lógica de la aplicación escribe palabras de datos por un lado de la FIFO. Por el otro lado, otra parte de la lógica de la aplicación lee esas palabras en el mismo orden (FIFO = primero en entrar, primero en salir).
Los datos se almacenan dentro de la FIFO. La profundidad de la FIFO es el número de palabras de datos que es capaz de almacenar. El ancho (es decir, el número de bits por palabra) y la profundidad son parámetros que el usuario configura para cada FIFO.
Las FIFO son probablemente el núcleo de IP (IP core) más utilizado en los diseños FPGA. Cuando una parte de la lógica genera datos y otra parte los consume, lo primero que se le ocurre a cualquiera es poner una FIFO entre ellas (eso no quiere decir, por supuesto, que una FIFO sea siempre la solución adecuada...).
Quien esté familiarizado con la interfaz de línea de comandos (en particular con UNIX/Linux) puede comparar el uso de las FIFO con las tuberías (pipes) entre comandos: la salida de un programa se convierte en la entrada del otro, y una maquinaria mágica entre ambos se encarga del resto.
Debido a su uso tan extendido, existe un acuerdo de facto sobre cómo debe comportarse una FIFO FPGA. Cualquier software de desarrollo de FPGA permite generar un módulo IP de FIFO para usarlo en el diseño de la aplicación. Y no solo eso: es muy probable que ese módulo de FIFO tenga un conjunto de puertos cuyo comportamiento sea igual que el de cualquier otra FIFO FPGA.
El software que suministra el fabricante de la FPGA permite crear FIFO a tu medida. Solo tienes que configurar sus atributos en alguna herramienta gráfica (ancho, profundidad y otros atributos que comentaré), y las herramientas se encargan del resto. Todo lo que te queda es hacer una instanciación (instantiation) del módulo en tu diseño. A diferencia de otras muchas tareas del mundo FPGA, esta tarea es realmente así de sencilla.
Como cada fabricante de FPGA presenta su propio núcleo de IP para FIFO, por supuesto es importante leer la documentación hasta la letra pequeña. Los distintos fabricantes de FPGA emplean una terminología ligeramente distinta para describir las FIFO, y los nombres de los puertos del módulo también son algo diferentes. Además, cada fabricante ofrece un conjunto un poco distinto de funciones adicionales y opciones de configuración.
Dicho esto, lo más probable es que los valores por defecto coincidan con lo que yo llamaré la «FIFO básica». Y, por encima de eso, hay un conjunto de funciones adicionales que están siempre disponibles.
Sin embargo, la implementación de la FIFO en el tejido lógico (logic fabric) difiere de un fabricante a otro; por eso, entender qué significan los atributos de la FIFO es importante para aprovechar bien los recursos de la FPGA.
En resumen, llegar a conocer y comprender las FIFO FPGA es un esfuerzo que solo se hace una vez. Cuando has aprendido a trabajar con ellas en una FPGA, es bastante fácil hacer lo mismo en otra, lo cual ya es, por sí solo, un motivo de su ubicuidad.
La FIFO básica
No existe ningún estándar escrito para las FIFO FPGA, pero, aun así, hay un acuerdo general sobre cómo deben comportarse.
Toda FIFO tiene dos interfaces: una para escribir palabras y otra para leerlas. Veamos una instanciación de lo que llamaré la «FIFO básica». Tiene variaciones importantes, que veremos más adelante.
myfifo myfifo_ins
(
.rst(rst), // Asynchronous reset input
// Write interface ports
.wr_clk(wr_clk), // Write clock input
.wr_en(wr_en), // Write Enable input
.din(din), // Write word input
.full(full), // Full output
// Read interface ports
.rd_clk(rd_clk), // Read Clock input
.rd_en(rd_en), // Read Enable input
.dout(dout), // Read word output
.empty(empty) // Empty output
);
Los nombres de los puertos son los que utilizan las herramientas de Xilinx, pero otros fabricantes de FPGA usan nombres parecidos.
Los puertos del módulo FIFO se dividen en tres grupos: una señal de reset (@rst), sobre la que volveré más adelante; una interfaz de escritura; y una interfaz de lectura. Como era de esperar, cada una consta de cuatro puertos.
Por lo que respecta a @din y a @dout, son dos puertos de tipo vector que transportan las palabras de datos que entran en la FIFO y salen de ella. El ancho de esas palabras se decide al configurar la FIFO con la herramienta de software correspondiente. También se configura la profundidad, es decir, cuántas palabras puede contener. Estos dos parámetros influyen en la cantidad de recursos de memoria de la FPGA que consume la FIFO.
Relojes
Conviene señalar que cada una de estas dos interfaces tiene su propio reloj: @wr_clk y @rd_clk. Los demás puertos de cada interfaz son síncronos con cada uno de estos dos relojes.
Las FIFO se emplean a menudo para mover datos de un dominio de reloj (clock domain) a otro, porque tienen dos relojes. Si una parte de la lógica de tu diseño es síncrona con clk_A, y otra parte lo es con clk_B, ¿cómo haces que trabajen juntas? Lo primero que piensa cualquier ingeniero de FPGA es poner una FIFO entre ellas. La razón principal es que la tarea de cruzar dominios de reloj (clock domain crossing) es un gran quebradero de cabeza, y usar una FIFO lo resuelve de forma fácil y segura.
La interfaz de escritura
La interfaz de escritura es sencilla: @wr_clk, @wr_en y @din son entradas de la FIFO, y @full es una salida.
Cuando @wr_en está en alto en el flanco ascendente de @wr_clk, el dato de @din se introduce en la FIFO. El puerto @full (llena) se pone en alto cuando la FIFO está llena.
Por ejemplo, esta es una forma de onda correspondiente a la escritura de cinco palabras en la FIFO:
En esta forma de onda, la lógica de la aplicación escribe primero las palabras D0 y D1. La FIFO pone su salida @full en alto para avisar de que se ha llenado tras la escritura correcta de D1. La lógica de la aplicación responde poniendo @wr_en en bajo durante ese mismo ciclo de reloj. Después de un par de ciclos, la FIFO pone @full en bajo para indicar que ya se puede volver a escribir. Lo más probable es que esto se deba a la actividad en el otro lado (es decir, que se hayan leído datos de la FIFO).
La lógica de la aplicación podría haber empezado a escribir en el ciclo de reloj en el que @full pasó a bajo; sin embargo, lo hace algo más tarde, en este ejemplo concreto. Como se ve en la forma de onda, se escriben tres palabras más.
En la forma de onda, cuando @din aparece marcado con el valor «Dx», significa que el valor se ignora y, por tanto, no importa lo que haya ahí. Por ejemplo, en el segmento con «Dx» entre D1 y D2, @din podría haber seguido con D1, haber cambiado a D2 antes de lo mostrado, o tener un valor completamente distinto. El resultado habría sido el mismo.
Para poner un ejemplo de código sencillo, supongamos que quiero llenar la FIFO con palabras que se van incrementando siempre que sea posible:
assign wr_en = !full;
always @(posedge wr_clk)
if (wr_en)
din <= din + 1;
Esto ejemplifica la relación correcta entre @full y @wr_en: si @full está en alto, @wr_en debe estar en bajo en ese mismo ciclo de reloj. ¿Y si no es así? ¿Qué ocurre si ignoramos la señal @full? Lo más probable es que la FIFO ignore @wr_en en ese caso. Así que probablemente se comportará como si su puerto @wr_en estuviera conectado a @the_real_wr_en, definido así:
assign the_real_wr_en = wr_en && !full;
Sin embargo, algunas herramientas de FPGA permiten configurar la FIFO sin ese mecanismo de seguridad. En ese caso, puede ocurrir prácticamente cualquier cosa si se intenta escribir datos cuando la FIFO está llena.
De una forma u otra, hay que respetar @full; de lo contrario, parecerá que los datos se han escapado. Piensa en el ejemplo anterior: si @wr_en hubiera estado siempre en alto, @din no habría dejado de incrementarse, independientemente de si los datos llegaban a escribirse o no. Así que, al leer los datos en el otro lado, la secuencia de incremento habría sido discontinua.
Observa que @full solo puede pasar de bajo a alto como resultado de un ciclo de escritura, es decir, inmediatamente después de un flanco ascendente de reloj en el que @wr_en esté en alto. La excepción es cuando se resetea la FIFO, como se comenta más adelante.
La interfaz de lectura
La interfaz de lectura es bastante parecida, aunque no exactamente igual. @rd_clk y @rd_en son entradas de la FIFO; @dout y @empty son salidas.
Cuando @rd_en está en alto en el flanco ascendente de @rd_clk, se lee una palabra nueva de la memoria de la FIFO y @dout se actualiza con su valor después de ese flanco ascendente, es decir, en el siguiente ciclo de reloj. El puerto @empty (vacía) se pone en alto cuando la FIFO está vacía.
En esta forma de onda de ejemplo se leen cinco palabras de la FIFO:
En esta forma de onda, la lógica de la aplicación empieza leyendo tres palabras. Cuando @empty pasa a alto, junto con la aparición de D2 en @dout, la lógica de la aplicación pone @rd_en en bajo en ese mismo ciclo de reloj. Como antes, habría sido perfectamente válido volver a poner @rd_en en alto en el mismo ciclo en que @empty pasó a bajo, porque se han escrito datos en la FIFO por el otro lado. En lugar de eso, esperó unos ciclos y siguió leyendo dos palabras más.
Y ahora una pequeña nota: si comparas esta forma de onda con la anterior, puede que notes que se escribieron cinco palabras y se leyeron cinco palabras. Entonces, ¿por qué @empty no se puso en alto junto con la aparición de D4? Bueno, porque quería mostrar que es perfectamente válido dejar de leer aunque la FIFO no esté vacía. Así que, para mantener este ejemplo imaginario, se escribieron más palabras en la FIFO y, por tanto, no se vació después de leer D4.
Observa que @dout mantiene su valor cuando @rd_en está en bajo. La lógica de la aplicación puede fiarse de esto: @dout contiene siempre el valor de la última palabra que se ha leído de la FIFO, salvo después del reset.
Aún más importante: el valor nuevo de @dout aparece después del flanco ascendente en el que @rd_en está en alto. Por eso la FIFO se comporta como el siguiente código Verilog:
always @(posedge rd_clk)
if (rd_en && !empty)
dout <= next_word_to_show;
Este código Verilog ficticio demuestra también que la mayoría de las FIFO ignoran @rd_en cuando @empty está en alto en ese mismo ciclo de reloj. Igual que con la interfaz de escritura, @rd_en no debería estar en alto si @empty está en alto en ese ciclo. Una vez más, a veces se puede configurar la FIFO para que no tenga este mecanismo de protección, así que no rompas esta regla.
@empty solo puede pasar de bajo a alto después de un ciclo de lectura, es decir, después de que @rd_en haya estado en alto en el flanco ascendente del reloj. La única excepción es cuando se resetea la FIFO.
Para poner un ejemplo, aquí hay un fragmento simplificado de código Verilog, sin reset, que lee palabras de la FIFO y calcula la suma acumulada:
assign rd_en = want_to_read_now && !empty;
always @(posedge rd_clk)
begin
rd_en_d <= rd_en;
if (rd_en_d)
sum <= sum + dout; // Don't try this at home: @sum is never reset.
end
Para la demostración, he añadido una señal @want_to_read_now, que indica que la lógica quiere leer. @rd_en, no obstante, solo está en alto si la FIFO no está vacía.
Fíjate en @rd_en_d, que contiene el valor de @rd_en con un retardo de un ciclo de reloj. En consecuencia, @rd_en_d está en alto justo cuando hay un valor nuevo y válido en @dout. Por eso se usa @rd_en_d como condición para consumir el valor de @dout. El retardo entre @rd_en y @dout complica un poco las cosas, como demuestra este ejemplo.
Sincronización y latencia
Como las formas de onda de escritura y de lectura las he dibujado por separado, no muestran un punto importante: pasan varios ciclos de reloj desde que se escribe la primera palabra en una FIFO vacía hasta que el puerto @empty pasa a bajo. Del mismo modo, pasan varios ciclos desde que se lee la primera palabra de una FIFO llena hasta que el puerto @full pasa a bajo.
Esto ocurre porque la información sobre la escritura en la FIFO debe propagarse a través de los dos dominios de reloj antes de llegar al otro lado de la FIFO. La lógica necesaria para cruzar entre dominios de reloj introduce un retardo de varios ciclos. Por eso la señal @empty responde un poco más tarde. Lo mismo ocurre con la señal @full.
¿De cuántos ciclos es ese retardo? Depende de muchas cosas, entre ellas de la relación temporal entre los flancos de los dos relojes en ese momento concreto. En resumen, es difícil saberlo.
Entre las cosas que afectan a ese retardo está el número de etapas de sincronización, que suele ser un parámetro configurable en la FIFO. Dos etapas es una opción habitual, pero se puede elegir un número mayor. Esto puede ayudar a aumentar la fiabilidad de la FIFO, a costa de usar algunos recursos de lógica más. También incrementa la latencia de los puertos @empty y @full, como acabamos de comentar.
Así que, si de verdad quieres consentir a tu FIFO, sube las etapas de sincronización a tres para sentirte absolutamente a salvo.
La entrada de reset
Todas las FIFO FPGA tienen una señal de reset. Como la FIFO utiliza dos relojes, no se espera que esta señal de reset sea síncrona con ninguno de ellos, así que es asíncrona (asynchronous reset). La lógica interna de la FIFO se encarga de sincronizar el reset internamente en cada uno de los dos dominios de reloj.
Entonces, ¿qué hace el reset? Para empezar, vacía la FIFO y pone @empty a nivel alto. Si había algún dato en la FIFO, se pierde.
En cuanto a la salida @full, es habitual, y recomendable, que las FIFO mantengan esta salida en alto después de un reset, hasta que la FIFO esté lista para recibir datos, es decir, para ciclos de escritura. Sin embargo, este comportamiento puede ser opcional, por lo que conviene comprobarlo en la documentación de la FIFO. Después de todo, la FIFO no está realmente llena tras un reset. Además, poner @full en alto por culpa del reset rompe la regla mencionada antes: @full solo debería pasar a alto como resultado de escribir datos.
Es importante saber que pasan algunos ciclos de reloj desde que la señal de reset se activa hasta que las señales @empty y @full pasan a alto. Esto se debe a la lógica de sincronización de la FIFO. Así que las cosas están un poco difusas durante los pocos ciclos que rodean la activación del reset. Asegúrate de que la lógica de la aplicación no intente escribir ni leer de la FIFO durante esos ciclos.
Aunque la señal de reset es asíncrona, debe ser la salida de un registro (flip-flop) de la FPGA. El reset no debe ser la salida de lógica combinacional (combinatorial logic), porque la FIFO podría recibir resets no deseados a causa de glitches.
De hecho, muchos ingenieros de FPGA asumen por error que basta con conectar prácticamente cualquier cosa al puerto de reset. Sin embargo, el fabricante de la FPGA puede tener especificaciones inesperadas sobre la señal de reset. Por ejemplo, esto está tomado de la guía de producto de Xilinx para su FIFO (PG057):
Si el reset asíncrono dura un único ciclo del reloj más lento y la activación se produce muy cerca del flanco ascendente del reloj más lento, puede que la detección del reset no se realice correctamente y se produzca un comportamiento inesperado. Para evitar estas situaciones, se recomienda siempre mantener el reset asíncrono activo durante al menos 3 [ ... ] ciclos del reloj más lento...
(Capítulo 3, "Resets")
Xilinx recomienda, por tanto, que el reset permanezca activo al menos tres ciclos de reloj. No sé cuántos ingenieros conoce esta recomendación. En cualquier caso, lee la guía de usuario de la FIFO de tu fabricante para saber cómo generar correctamente esta señal de reset.
Cómo se implementa la FIFO
Aunque las herramientas de software del fabricante se encargan de que la FIFO funcione correctamente, conviene saber qué recursos de la FPGA se utilizan, sobre todo para evitar quedarse sin recursos de algún tipo.
Cada FPGA tiene sus opciones, pero mencionaré brevemente algunas habituales:
- Totalmente implementada en hardware. Normalmente esto significa que se utiliza una RAM de bloques (block RAM) para el almacenamiento y que, además, la lógica que controla la FIFO está implementada en silicio y no en el tejido lógico. Esto no ahorra demasiada lógica, pero la implementación en silicio probablemente sea mejor para frecuencias altas. El principal inconveniente es que las funciones de una FIFO así se limitan a lo que está implementado en silicio, por lo que la FIFO puede tener un tamaño relativamente limitado y pueden faltar funciones triviales.
- FIFO de RAM de bloques (block RAM). Es el tipo más común. La FIFO está formada por las RAM de bloques que sean necesarias para conseguir el ancho y la profundidad deseados. La lógica que controla la FIFO se implementa en el tejido lógico.
- FIFO de RAM distribuida (distributed RAM). Son como las FIFO de RAM de bloques, pero se utilizan slices (slice) de lógica como RAM en lugar de RAM de bloques. Recuerda que la mayoría de las FPGA pueden usar las LUT de los slices como RAM, así que esta opción es especialmente adecuada cuando la FIFO es poco profunda. En general suele ser adecuada para profundidades de 32 palabras o menos, pero el equilibrio depende del diseño y de la familia de FPGA.
- FIFO basadas en registros de desplazamiento. Es la versión exótica de las FIFO de RAM distribuida: como los slices también pueden comportarse como registros de desplazamiento, es posible ahorrar algo de lógica aprovechando esta circunstancia.
Con esto terminamos la primera página de esta serie sobre las FIFO. La siguiente página trata sobre variantes habituales de las FIFO y sus funciones adicionales.

