01signal.com

Resets asíncronos en FPGA: no es tan fácil como muchos creen

Esta página es la primera de tres en una serie sobre resets en FPGAs. Como muchos usan resets asíncronos (asynchronous reset) sin saber que en realidad no funcionan como esperan, esta primera página explica por qué todo el tema no es tan fácil como parece.

¿De verdad funciona tu reset?

Considera este ejemplo para resetear una máquina de estados (state machine), que es incorrecto:

   always @(posedge clk or negedge resetn)
     if (!resetn)
       state <= ST_START;
     else
       case (state)
	 ST_START:
	   begin
	      state <= ST_NEXT;
	      [  ... do some stuff maybe? ... ]
	   end

	 ST_NEXT:
	   begin
	      [ ... do something ... ]
	   end
       endcase

Te preguntarás: ¿qué tiene de malo el reset? ¡Es exactamente como lo muestran los libros de texto! Un reset asíncrono activo por nivel bajo, que lleva el registro que implementa @state a su estado inicial. ¿Qué podría salir mal?

Para debatir el asunto, supongamos que la señal @resetn en sí está bien. Es decir, no está conectada directamente a un pulsador ni nada parecido. A lo mejor se usó un chip diseñado para generar una señal de reset, o el reset se genera internamente en la FPGA. De una forma u otra, supondremos que el reset se activa de forma estable, que permanece activo bastante tiempo y que luego se desactiva. Aun así, es incorrecto.

Y cuando digo incorrecto, me refiero al tipo de fallo que hace que la FPGA se comporte de forma extraña de vez en cuando, sin razón aparente.

Entonces, ¿cuál es el problema? Pues que, como el reset permanece activo el tiempo suficiente, seguro que lleva a @state a su estado inicial. Pero ¿qué ocurre cuando se desactiva (es decir, cuando vuelve a '1' en el ejemplo anterior)? Es entonces cuando los flip-flops en cuestión deberían empezar a responder a los flancos de subida de @clk.

Sin embargo, el flip-flop tarda un poco en recuperarse de la señal de reset y en empezar a realizar el muestreo (sampling) de la entrada de datos en los flancos de subida del reloj. Y como el reset es asíncrono por definición, puede desactivarse en cualquier momento con respecto a @clk.

Si el primer flanco de subida de @clk llega demasiado pronto después de la desactivación del reset, el flip-flop ignora ese flanco. Eso no importa si todos los flip-flops conectados a @clk hacen lo mismo. Pero no todos los flip-flops son exactamente iguales: algunos reciben el flanco de reloj un poco antes que otros, y otros reciben la desactivación del reset más tarde.

A decir verdad, esta explicación es un poco simplista. Para una visión más precisa del problema, consulta la parte sobre Recovery y Removal en la página que explica los fundamentos de temporización.

En resumidas cuentas: con un poco de mala suerte, el reset puede desactivarse lo bastante cerca del flanco de subida del reloj como para que algunos flip-flops respondan al primer flanco y otros lo ignoren. De hecho, algunos flip-flops pueden tardar un poco más en decidir qué hacer. Échale la culpa a las diferencias entre flip-flops del mismo chip, o a la desviación del reloj (clock skew) y la desviación del reset: el caso es que algunos flip-flops van un ciclo de reloj por delante de los demás.

Para entender lo malo que es esto, considera el ejemplo anterior. Si el sintetizador (synthesizer) reconoce que se trata de una máquina de estados, es muy probable que implemente la variable de estado con codificación one-hot. En otras palabras, asigna un registro de un bit a cada estado. Cada uno de estos registros está activo cuando la máquina de estados se encuentra en el estado correspondiente. Supongamos que el sintetizador asignó un registro llamado hot_state_0 para el estado llamado ST_START, y hot_state_1 para ST_NEXT. Claramente, el reset activa hot_state_0 y desactiva hot_state_1.

Observa ahora que la máquina de estados pasa incondicionalmente de ST_START a ST_NEXT. Por tanto, hot_state_0 se desactiva en el primer flanco de reloj tras la desactivación del reset, y hot_state_1 se activa.

Pero ¿qué ocurre si el reset se desactiva en un momento desafortunado, de modo que algunos flip-flops no respondan al primer flanco y otros sí? Una posibilidad es que hot_state_0 no responda al primer flanco, mientras que hot_state_1 sí responda. El resultado es que ambos se activan, lo cual es una condición ilegal con codificación one-hot. Si ocurre al revés, los dos registros quedan inactivos, así que en realidad todos los registros one-hot de la máquina de estados quedan desactivados. En cualquiera de los dos casos, la máquina de estados puede no ser capaz de volver a un estado legal.

¿Cómo se manifestará esto en la práctica? Depende de la aplicación, por supuesto, pero lo más probable es que algo no funcione correctamente hasta que se aplique un reset a la FPGA de nuevo. Encontrar la causa de este comportamiento puede ser enormemente difícil, porque el problema aparecerá de forma aleatoria, y no necesariamente con frecuencia. Además, es probable que se comporte de forma distinta entre una compilación del diseño FPGA y otra, y quizá de una placa a otra. En resumen, es el tipo de fallo que puede volverte loco. Quizá no suene tan grave en esta discusión, porque el origen del problema es el tema de esta discusión. Pero cuando aparece una inestabilidad así en la vida real, puede manifestarse de cualquier manera, y muchas veces da la sensación de que la FPGA está embrujada.

¡Pero yo hago esto todo el tiempo y funciona!

Cierto. En la gran mayoría de los casos, no importa demasiado que algunos flip-flops no respondan al primer flanco de reloj después del reset.

La razón principal por la que el ejemplo de la máquina de estados puede fallar es que abandona el estado inicial en el primer ciclo de reloj. La mayoría de las máquinas de estados en diseños reales tienen una regla para salir del estado inicial, de modo que se quedan en ese estado durante los primeros ciclos de reloj. Así que uno se libra de este error.

Pero aquí tienes otro ejemplo. Un contador sencillo:

   reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter + 1;

En este caso, @counter está formado por 16 flip-flops. Cada uno de estos flip-flops recibe en su entrada de datos el valor que tendrá el contador en el siguiente ciclo de reloj, y también recibe @resetn en su entrada de reset asíncrono.

Cuando @resetn está activo, @counter toma el valor 0, y el valor del contador para el siguiente ciclo es 1. Por tanto, todos los flip-flops excepto counter[0] seguirán en cero, tanto si no responden al primer flanco como si sí. Así que el contador empezará a contar correctamente en cualquier caso. En la gran mayoría de los casos en que se escribe un código como este, no importa si el contador se pierde el primer ciclo de reloj o no.

Esto es, sin embargo, otra historia:

   reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter - 1;

Una diferencia pequeña, pero significativa: si el contador empieza en cero y cuenta hacia abajo, el valor del contador en el siguiente ciclo de reloj es 0xffff. En otras palabras, todos los flip-flops deben cambiar su valor en el primer flanco después del reset. Por tanto, si algunos responden al primer flanco de reloj tras el reset y otros no, el contador puede arrancar prácticamente en cualquier valor aleatorio.

Pero ¿quién resetea un contador a cero para luego contarlo hacia abajo?

Pues aquí tienes un ejemplo más realista: una señal de habilitación de reloj (clock enable) que hace que la lógica se comporte como si la frecuencia del reloj se redujera a la mitad (y que, por tanto, permite un camino multciclo (multi-cycle path) si hace falta):

   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= !en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

Uno podría pensar que aquí no puede fallar nada: la habilitación de reloj, @en, es un único registro, así que no importa cuándo empiece a conmutar... ¿o sí? La cuestión es que una señal de habilitación de reloj suele tener un fan-out elevado, así que el sintetizador puede duplicarla para no superar el límite de fan-out.

En mi experimento, un tanto anecdótico, con el sintetizador de Vivado se vio que cada uno de los registros duplicados que implementaban @en dependía de su propia señal de salida. En otras palabras, no había una única señal que todos los flip-flops usaran para decidir cuál debía ser su siguiente salida. Más bien había muchos flip-flops independientes que siempre cambiaban su valor en un flanco de subida del reloj. Por tanto, si estos flip-flops no empiezan a conmutar en el mismo ciclo de reloj, sus salidas permanecerán diferentes indefinidamente.

Si se produce semejante accidente, lo más probable es que la lógica falle por completo. Así que, si insistes en usar un reset asíncrono, asegúrate al menos de que todas las habilitaciones de reloj dependan de una única fuente, por ejemplo así:

   reg pre_en; // Apply some don't-touch synthesis directive on this
   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       pre_en <= 0;
     else
       pre_en <= !pre_en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= pre_en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

El truco está en que @pre_en es el registro que decide el siguiente valor. Este flip-flop tiene un fan-out bajo y, posiblemente, algún atributo que le dice al sintetizador que no lo toque. Así se garantiza que sea un único registro. Todos los flip-flops que implementan @en dependen de @pre_en, de modo que todos coinciden en el valor de @en en el siguiente flanco. En cuanto al primer flanco después del reset, no importa que algunos flip-flops no lo detecten, porque el valor de @en en el primer ciclo es cero de todas formas.

Así que, en resumidas cuentas, usar mal un reset asíncrono funcionará bien por lo general, sobre todo porque la lógica normalmente tolera la incertidumbre sobre cuándo se desactiva el reset respecto al reloj. Aun así, aplicar resets asíncronos sin cuidado puede provocar comportamientos erráticos ocasionales que son muy difíciles de resolver.

Usar una restricción de temporización entre el reset y el reloj

La forma aparentemente obvia de evitar la relación temporal incierta entre la desactivación del reset y el flanco de subida del reloj es usar una restricción de temporización (timing constraint) sobre la señal de reset. Sin embargo, al hacerlo, la señal de reset pasa a ser síncrona.

Pero, ¿con qué reloj es síncrona la señal de reset? A menudo conviene tener un único reset asíncrono global para todo el diseño lógico. Ese reset se genera con lógica que es síncrona de un reloj concreto. Si ese reset se usa en lógica que es síncrona de otro reloj distinto, tenemos un cruce de dominios de reloj (clock domain crossing). Este es un tema aparte, pero lo más importante es la posibilidad de que las herramientas ignoren la temporización en los caminos (paths) relevantes.

Por tanto, el punto de partida para usar restricciones de temporización sobre resets asíncronos es que debe haber un reset asíncrono independiente para cada reloj. O, si insistes, un reset asíncrono separado para cada grupo de relojes relacionados (related clocks). De lo contrario, una restricción de temporización no tiene ningún sentido. Si te suena raro, es porque el reset asíncrono ya no es asíncrono.

El hecho de que el código Verilog use el patrón típico de un reset asíncrono no cambia nada. Tampoco importa que se utilice la entrada de reset asíncrono del flip-flop, ni que el flip-flop esté configurado para considerar su entrada de reset como asíncrona: si el reset es síncrono con un reloj y se usa una restricción de temporización, en la práctica el reset es síncrono. Para este caso, puedes plantearte usar directamente el patrón de Verilog correspondiente:

   always @(posedge clk)
     if (!resetn)
       state <= ST_START;
[ ... ]

Dicho esto, el vídeo de Intel en YouTube sobre cierre de temporización (timing closure) recomienda usar resets asíncronos que son síncronos con un reloj, y también usar restricciones de temporización. Se elige un reset asíncrono para aprovechar los recursos dedicados al encaminamiento global dentro de la FPGA. Me parece bastante extraño, porque incluso el encaminamiento global puede tener un retardo grande, sobre todo en FPGAs grandes. Pero seguro que hay algunos escenarios donde esto tiene sentido.

En realidad, hay otra razón para quedarse con el reset asíncrono incluso cuando es efectivamente síncrono, que se comenta en la siguiente página: permite propagar la activación de la señal de reset de forma asíncrona, algo útil tanto en simulaciones como en pruebas de ASICs.

Si se usa este método con un reset asíncrono sincronizado, es importante asegurarse de que la restricción de temporización se cumpla en todos los caminos de señal que llegan a las entradas de reset asíncrono de los flip-flops. El hecho de que el reset lo genere un flip-flop síncrono con el mismo reloj que el flip-flop de destino no garantiza por sí solo que el camino entre ambos esté cubierto por la temporización. Por defecto, algunas herramientas de temporización ignoran cualquier camino que termine en entradas asíncronas, así que puede que haya que activar esa cobertura explícitamente. Asegúrate de revisar en los informes de temporización que estos caminos están efectivamente cubiertos por las restricciones de temporización. Es fácil caer en este error.

Con esto terminamos la primera página de esta serie sobre resets. En la siguiente página se comentan las distintas opciones para los resets y la inicialización de la FPGA.

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)