Esta es la segunda página de una serie sobre resets en FPGAs. Después de explicar en la página anterior por qué los resets asíncronos (asynchronous resets) no son lo que muchos creen, esta página trata las distintas opciones para los resets y la inicialización de la FPGA.
Primero de todo: ¿qué es un reset?
Venga ya, todos sabemos lo que es un reset. Es como ese botón del ordenador que pulsas y todo vuelve a empezar de cero. Es esa señal que llega a un chip y que garantiza que, sin importar lo que haya pasado antes, a partir de ese momento todo va a funcionar. Algunos se refieren al reset como la señal que lleva el sistema a un estado conocido.
Para los diseñadores de FPGAs, el reset suele ser simplemente esa entrada extra que hay que añadir a cada módulo y usar en un patrón de código repetido. Algo que se hace sin más, sin prestar necesariamente atención a qué garantiza realmente esa señal de reset, si es que garantiza algo, o si se podría omitir por completo.
El error más común es centrarse en que la señal de reset lleva el sistema a un estado conocido. Eso es cierto, por supuesto, pero lo importante es lo que ocurre después de que el reset se desactive. Hay que garantizar que la lógica empiece a funcionar de forma predecible. O al menos, lo bastante predecible como para que funcione correctamente. No sirve de nada resetear un sistema si no estamos seguros de su comportamiento después de desactivar el reset.
Lo que hace que este tema sea especialmente difícil es que interviene la suerte: por lo general, el estado del sistema cuando el reset se activa es desconocido y aleatorio, y también lo es el momento en que el reset se desactiva. Un manejo inadecuado de la señal de reset puede provocar fallos raros que aparecen en momentos aleatorios, y que parecen ser un problema completamente distinto. Del mismo modo, es posible ignorar este asunto sin consecuencias visibles, salvo problemas ocasionales que normalmente se tratan con brujería.
Del mismo modo que hace falta usar bien las restricciones de temporización (timing constraints) y manejar bien los relojes y los dominios de reloj (clock domains), también es imprescindible tratar adecuadamente el despertar y el reset de la FPGA para que esta funcione de forma fiable. Lo común en todos estos temas es que uno puede permitirse ignorarlos, y bastantes ingenieros lo hacen, pero desgraciadamente a costa de una FPGA que se comporta como si estuviera embrujada de vez en cuando.
Simulación frente a hardware
Esta página se centra en cómo las decisiones sobre los resets influyen en el diseño cuando se carga en la FPGA. Pero, evidentemente, estas decisiones también tienen un impacto en la simulación de la lógica.
Es importante distinguir entre estas dos situaciones. Los simuladores asignan el valor inicial X (desconocido) a todos los registros, en particular en la simulación de comportamiento. Estos valores X se propagan después a cualquier registro que dependa de un valor X en su función lógica. De ahí que incluso un único registro con un valor X inunde todo el diseño de X y la simulación se vuelva inservible.
Una solución incorrecta habitual a este problema es poner un reset asíncrono (asynchronous reset) a todos los registros del diseño para deshacerse de todas las X. Normalmente se hace activando brevemente el reset al principio de la simulación. Como resultado, todos los registros obtienen un valor conocido y todo parece perfecto. Desgraciadamente, esta solución incorrecta suele dar una ilusión de arranque limpio, ocultando los problemas que se comentan en la página anterior.
Incluso cuando los resets se usan correctamente, la opción perezosa de resetear todos los registros para no tener que perseguir el origen de los valores X en la simulación no es buena idea. Esto no solo malgasta recursos y quizá haga más difícil cumplir las restricciones de temporización: resetear registros innecesariamente también puede ocultar errores, porque una inundación de X puede tener su origen en una dependencia no intencionada de un registro respecto a otro. Por tanto, esa inundación de X puede ser una advertencia de que algo anda mal en el diseño.
En comparación con la simulación, el hardware es mucho más tolerante con la ausencia de resets. Pero cuando los resets se usan incorrectamente, o no se usan en absoluto cuando son necesarios, el hardware puede comportarse de forma bastante inesperada.
En conclusión, los resets deben usarse pensando en el hardware, y no para eliminar las molestas X durante la simulación. Y como el foco debe estar en el hardware, estas pocas palabras son todo lo que tengo que decir sobre simulaciones.
Estrategias para resetear
La decisión sobre si aplicar resets a los registros y cómo hacerlo exige considerar el caso de cada registro por separado. Hacerlo no solo evita un fan-out innecesariamente alto de las señales de reset, sino que también es una buena oportunidad para pensar si está garantizado que la lógica arranque correctamente después del reset, sin importar su estado anterior.
En principio, hay cuatro opciones:
- Aceptar que el valor inicial del registro es desconocido.
- Confiarse en el valor inicial configurable del elemento síncrono de la FPGA. Es decir, sin reset explícito.
- Usar un reset asíncrono. Por ejemplo:
always @(posedge clk or negedge resetn) if (!resetn) counter <= 0; else counter <= counter + 1; - Usar un reset síncrono. Por ejemplo:
always @(posedge clk) if (!resetn) counter <= 0; else counter <= counter + 1;
Mi opinión
Cada una de estas opciones se comenta más abajo, así que primero presentaré lo que considero la forma correcta de hacerlo, y luego desarrollaré el tema:
- Repasa, por separado, cada registro (y otros elementos lógicos que tengan entrada de reset) del diseño, y comprueba si las dos primeras opciones tienen sentido. En otras palabras, si se puede evitar el reset.
- Si no es así, prefiere un reset síncrono (synchronous reset).
- Usa resets asíncronos solo con elementos lógicos que lo exijan explícitamente (en particular primitivas FPGA complejas (primitive) y núcleos IP (IP cores), p. ej. transceptores y controladores de bus), y aun así intenta usar la señal de reset síncrono si es posible.
- Resetea siempre las máquinas de estados (state machines) correctamente. Aunque su definición de comportamiento garantice que antes o después alcanza un estado conocido, asegúrate de que un reset explícito y seguro la lleve a un estado conocido. Esto se debe sobre todo a que el sintetizador (synthesizer) puede implementar la variable de estado de una forma diferente debido a la optimización (en particular one-hot). Esa representación de la variable de estado podría no converger nunca a un estado legal a menos que la máquina de estados se resetee explícitamente.
- Resetea siempre los núcleos IP, bloques de diseño y primitivas que tengan entradas de reset si la documentación lo exige (y en cualquier caso es buena idea hacerlo). Puede que funcionen perfectamente sin el reset, pero eso puede ser pura suerte. Aunque el reset esté relacionado con una funcionalidad que no uses, e incluso aunque el reset parezca limitarse a poner a cero algunas salidas que no necesitas en tu diseño, la regla es sencilla: en lo que respecta a los resets, sigue la documentación al pie de la letra. Aunque no haya una razón aparente para que sea necesario.
Estas sugerencias coinciden bastante con lo que recomiendan hoy en día los fabricantes de FPGAs.
También añadiré una pauta muy general: resetea siempre la lógica de control explícitamente y deja que los caminos de datos (data paths) se purguen solos de sus datos basura iniciales.
Y ahora viene la discusión extensa sobre cada opción.
Opción nº1: Valor inicial desconocido
Algunos registros no necesitan ningún reset ni valor inicial. Esto se aplica en particular a los registros de desplazamiento y elementos lógicos similares. Por lo general, es muy probable que los caminos de datos encajen en esta opción.
Considera este fragmento:
reg [31:0] d0, d1, d2, d3, d4;
always @(posedge clk)
begin
d4 <= d3;
d3 <= d2;
d2 <= d1;
d1 <= d0;
d0 <= orig_data;
end
Se trata claramente de cinco registros de retardo de 32 bits cada uno. En algunas FPGAs (Xilinx en particular), el sintetizador lo detecta como un registro de desplazamiento si solo se usa el último valor (@d4), y no hay ningún reset en estos registros. Esto puede reducir el consumo de lógica de forma considerable.
Evidentemente, la lógica conectada a la salida de estos registros de retardo debe ser capaz de tolerar algunos datos aleatorios iniciales. Un caso típico en el que esto no es un problema es cuando hay algún otro registro o máquina de estados que está correctamente reseteado y se asegura de que los valores no inicializados se ignoren cuando llegan. Por ejemplo, si estas líneas de retardo están relacionadas con un pipeline, la lógica de control del pipeline ignorará de forma natural los datos inválidos.
En términos más generales, la oportunidad de no resetear ni inicializar un registro se reconoce fácilmente cuando hay una bandera o estado que lo acompaña y que indica su validez. O cuando hay una secuencia clara en la que primero se asigna un valor al registro y luego se consume ese valor. En resumen, cuando es fácil saber que el valor del registro se ignora hasta que se le haya asignado un valor apropiado.
Otro tipo de valor inicial desconocido es cuando puede depender del sintetizador. Por ejemplo:
reg val;
always @(posedge clk)
val <= 1;
El sintetizador puede decidir que @val es un cable que tiene un valor constante de 1. Otra posibilidad es asignar un registro con cero como valor inicial, de modo que cambie a 1 en el primer flanco de reloj. Lo que ocurra en realidad depende del sintetizador. Así que, aunque el valor de @val sea siempre conocido excepto en el primer ciclo de reloj, el valor inicial debe considerarse desconocido.
Opción nº2: Valores iniciales de la FPGA
El valor inicial de los elementos síncronos básicos de una FPGA (normalmente flip-flops, registros de desplazamiento y memorias) se define en el bitstream de configuración. El uso más conocido de esta característica es crear una ROM asignando un bloque de RAM con valores iniciales y sin escribir nunca en ella.
Otro aspecto conocido de esta característica es que una FPGA normalmente despierta de la configuración con aparentemente todos los registros a cero. Esto se debe a que el sintetizador suele asignar a todos los registros el valor cero como valor inicial, pero puede haber sorpresas a menos que el valor inicial se establezca explícitamente.
Algunos elementos síncronos, en particular registros de desplazamiento y bloques de RAM dedicados, pueden crearse describiendo su comportamiento en Verilog o VHDL (por inferencia): el sintetizador normalmente crea un registro de desplazamiento cuando el código se parece a una línea de retardo. Del mismo modo, un elemento lógico de RAM se crea como respuesta a un array. Sin embargo, si se usa un reset en estos registros (reset síncrono o asíncrono), se impide que el sintetizador utilice los recursos lógicos de esta manera. Esto se debe a que ni los registros de desplazamiento ni las RAMs tienen una entrada de reset que establezca los valores de la memoria interna.
¿Y cómo se establecen los valores iniciales? Durante el proceso de configuración, a todos los elementos síncronos se les da su valor inicial justo antes de que la FPGA se active, es decir, antes de que los elementos síncronos empiecen a responder a sus relojes y a sus entradas de reset asíncrono. En los dispositivos de Xilinx, esto se implementa mediante la señal Global Set Reset (GSR), que pone todos los elementos síncronos en su estado inicial. Después se activa Global Write Enable (GWE), que hace que los elementos síncronos funcionen con normalidad.
Como el proceso de configuración se lleva a cabo independientemente de cualquiera de los relojes que usa la lógica de aplicación de la FPGA, la transición de la FPGA a su estado operativo es asíncrona respecto a cualquiera de esos relojes. En consecuencia, los elementos síncronos se comportan exactamente como si se desactivara un reset asíncrono sin tener en cuenta ningún reloj. En otras palabras, es posible que algunos elementos síncronos respondan al primer flanco de reloj que llega después de la transición al estado operativo, mientras que otros no respondan a ese flanco por una violación de temporización. Eso puede provocar errores muy desagradables, como se comentó en la primera página de esta serie.
Es importante tener en cuenta que los relojes no son necesariamente estables cuando la FPGA despierta. Si los generan los propios PLLs de la FPGA, pueden violar las restricciones de temporización de forma descontrolada. Como se ha comentado antes, esto no es un problema si el reloj se ignora hasta que esté estable (p. ej. mediante una habilitación de reloj (clock enable)), o si se sabe que está estable cuando la FPGA despierta. También es aceptable si el diseño garantiza que ningún elemento síncrono tiene motivo para cambiar su valor hasta que el reloj sea estable. De lo contrario, establecer un valor inicial no garantiza gran cosa.
A pesar de las limitaciones de establecer el valor inicial, en muchos escenarios es suficiente y no hace falta un reset explícito. Y en algunos casos, sencillamente no hay elección, porque no se dispone de una señal de reset. Por ejemplo, la lógica que crea las señales de reset de la FPGA inmediatamente después de que despierte. Un ejemplo de esa lógica se muestra en la tercera página de esta serie.
Con la mayoría de los sintetizadores, establecer el valor inicial de un registro es muy sencillo: se usa el "initial" de Verilog:
reg [15:0] counter;
initial counter = 1000;
Puede resultar sorprendente que se pueda usar "initial" en código Verilog para síntesis, pero resulta que este uso está ampliamente soportado. Así que esta palabra clave es sin duda el método preferido si el sintetizador la soporta (es decir, si la documentación menciona explícitamente este uso de "initial"). Además, el método alternativo suele ser específico del fabricante, y a veces incluso específico de una familia de FPGA. Así que, aunque "initial" no siempre sea portable, probablemente siga siendo la opción más portable.
El método alternativo para establecer el valor inicial depende de la FPGA que se utilice. Este método suele consistir en una instanciación (instantiation) del elemento síncrono como primitiva (primitive), y en asignar el valor inicial como parámetro de instanciación. Por ejemplo, un flip-flop de Xilinx:
FDCE myflipflop (
.C(clk),
.D(in),
.Q(out),
.CLR(1'b0),
.CE(1'b1)
);
defparam myflipflop.INIT = 1;
Usar "initial" es mucho más bonito, ¿verdad?
Opción nº3: Reset asíncrono
Si todavía no has leído la página sobre por qué los resets asíncronos se usan a menudo incorrectamente, te sugiero que lo hagas primero. A menos que no tengas intención de usar este tipo de reset de ninguna manera.
Por alguna razón, mucha gente considera el reset asíncrono la solución correcta para todo. A lo mejor porque aparece a menudo en ejemplos de código, quizá por la ilusión de tener una forma sencilla de lograr un reset global que llegue a todos los elementos síncronos. Quizá porque en el viejo mundo de los ASICs, el reset asíncrono era útil para el test del chip durante el proceso de fabricación, ya que permite resetear todo el chip y empezar a aplicar vectores de test.
Así que vamos a la realidad: la forma correcta y limpia de usar un reset asíncrono es hacerlo con los relojes apagados. Eso es lo que significa realmente la parte de "asíncrono". En un diseño práctico, esto consiste en las siguientes etapas:
- Desactiva todos los relojes (excepto el reloj que usa la lógica que controla este procedimiento). Esta desactivación suele hacerse desactivando la entrada de habilitación de reloj (clock enable) de los buffers globales de reloj (clock gating).
- Activa y desactiva la señal de reset asíncrono. Asegúrate de que el pulso sea lo bastante largo como para resetear todos los elementos síncronos.
- Espera el tiempo suficiente para garantizar que todos los elementos síncronos están listos para recibir relojes.
- Reactiva los relojes.
Esta secuencia no es difícil de implementar, pero puede ser más difícil garantizar que el primer flanco de cada reloj esté bien formado, de modo que no haya glitches: un problema común con los buffers de reloj es que existe un requisito sobre la temporización entre la activación de la habilitación de salida del buffer de reloj y el primer flanco de reloj que debe pasar a través del buffer. Si se viola este requisito de temporización, el buffer de reloj puede sacar un glitch (un pulso corto que viola los requisitos de la FPGA sobre el reloj). Eso puede provocar un comportamiento impredecible de todos los elementos síncronos que dependen de ese reloj.
Desgraciadamente, la documentación de los fabricantes de FPGAs no siempre explica cómo garantizar este requisito de temporización. Como resultado, puede que no sea posible asegurar que el primer flanco de reloj después del reset se comporte correctamente. Y sin una garantía sobre el primer flanco, el reset no sirve de nada.
Si usas este método, asegúrate de que no se apliquen restricciones de temporización sobre los caminos (paths) relacionados con el reset asíncrono. Tal imposición no es necesaria en este caso, y puede estar activada por defecto.
Hay un método alternativo para aplicar un reset asíncrono de forma fiable, que se suele sugerir. Este método no implica apagar los relojes, y por tanto no depende de los buffers de reloj: la idea es añadir unos pocos flip-flops que dejan pasar la activación de la señal de reset asíncrono directamente, pero que desactivan el reset de forma síncrona. En otras palabras, un reset asíncrono sincronizado.
Por ejemplo, si el reset asíncrono original es @external_resetn, esto genera un reset de este tipo:
reg pre_rstn1, pre_rstn2;
reg resetn;
always @(posedge clk or negedge external_resetn)
if (!external_resetn)
begin
resetn <= 0;
pre_rstn2 <= 0;
pre_rstn1 <= 0;
end
else
begin
resetn <= pre_rstn2;
pre_rstn2 <= pre_rstn1;
pre_rstn1 <= 1;
end
Ten en cuenta que @clk es el reloj que usan los elementos síncronos que resetea @resetn.
Cuando @external_resetn está activo (es decir, en bajo), los tres registros se activan (es decir, se ponen a cero) de forma asíncrona. Sin embargo, cuando @external_resetn se desactiva, solo @pre_rstn1 se desactiva en el siguiente flanco de reloj, y esto se propaga a @pre_rstn2 y @resetn en los flancos siguientes.
La finalidad de los dos registros extra es protegerse contra la metaestabilidad (metastability), de modo que @resetn se desactive de forma segura. Esto es necesario si @external_resetn se desactiva con una temporización mala respecto a @clk, lo que puede producir una condición metaestable en @pre_rstn1 (esta página explica la metaestabilidad).
La ventaja de este sincronizador es que @external_resetn puede usarse como un reset asíncrono: funciona incluso si no hay ningún reloj activo. No obstante, los elementos síncronos reciben una señal de reset que se desactiva de forma síncrona, así que la temporización puede quedar garantizada.
Casi sobra decir que cada reloj necesita su propio reset asíncrono sincronizado.
Es importante tener en cuenta que generar @resetn como se muestra arriba no es suficiente. Las restricciones de temporización para @clk deben imponerse sobre los caminos que van de @resetn a los elementos síncronos. Por defecto, algunas herramientas de FPGA ignoran la temporización de los caminos que terminan en una entrada de reset asíncrono de un elemento síncrono. Puede que sea necesario cambiar algún ajuste de la herramienta para este fin.
Así que si @resetn se usa como un reset asíncrono normal, p. ej.
always @(posedge clk or negedge resetn)
if (!resetn) // Are you sure this path is timed?
the_register <= 0;
else
[ ... ]
entonces el sincronizador mostrado arriba no es suficiente para garantizar una recuperación fiable del reset. Es tu responsabilidad verificar que los caminos que empiezan en @resetn y terminan en las entradas de reset asíncrono de los flip-flops están efectivamente cubiertos por la temporización.
También es importante señalar que este sincronizador no protege contra glitches en @external_resetn: si la duración del pulso activo de @external_resetn es más corta que la especificación de los flip-flops de la FPGA, puede pasar cualquier cosa. Así que @external_resetn debe generarla alguna lógica o electrónica externa que garantice un pulso largo. Si eso no es posible, la única solución es sincronizar el reset por completo, como en este ejemplo para @sync_resetn:
reg pre_rstn1, pre_rstn2;
reg sync_resetn;
always @(posedge clk)
if (!external_resetn)
begin
sync_resetn <= 0;
pre_rstn2 <= 0;
pre_rstn1 <= 0;
end
else
begin
sync_resetn <= pre_rstn2;
pre_rstn2 <= pre_rstn1;
pre_rstn1 <= 1;
end
Pero este sincronizador ignora @external_resetn si @clk está inactivo. Esto es problemático si se supone que la lógica debe tratar @external_resetn como un reset asíncrono. En otras palabras, que debería funcionar incluso cuando los relojes no están activos.
Volvamos al primer sincronizador. ¿Qué tal usar @resetn como un reset síncrono normal? Algo así:
always @(posedge clk) // @resetn not in sensitivity list!
if (!resetn)
the_register <= 0;
else
[ ... ]
Esto es más o menos correcto, porque la desactivación de @resetn está ciertamente cubierta por las restricciones de temporización. Sin embargo, la activación asíncrona de @resetn no está cubierta, así que los elementos síncronos afectados pueden comportarse de forma aleatoria justo antes de que el reset haga efecto. Es mejor usar un reset completamente sincronizado para este propósito, p. ej. @sync_resetn tal como se ha definido arriba.
Terminaré este tema con un comentario general: he elegido usar resets activos por nivel bajo en los ejemplos anteriores, sobre todo por una tradición que viene de los tiempos en que la señal de reset se generaba con un condensador conectado a la tensión de alimentación a través de una resistencia. Como el condensador no tenía tensión inicialmente, la entrada de reset estaba a '0'. Este condensador acumulaba carga bastante pronto y, en consecuencia, la entrada de reset pasaba a '1'. Este antiguo tipo de reset de encendido es la razón de que muchos resets sigan siendo activos por nivel bajo hoy en día.
En conclusión, es posible usar un reset asíncrono de forma fiable; sin embargo, conseguirlo no es, desde luego, tan sencillo como muchos creen. He presentado dos formas de garantizar un reset asíncrono fiable: o bien apagando los relojes temporalmente para evitar problemas de temporización, o bien usando un sincronizador para garantizar la temporización. Como suele ocurrir con las FPGAs, la temporización es el nombre del juego.
En la mayoría de los diseños reales que dependen de un reset asíncrono, no se usa ninguno de estos métodos. Como resultado, la fiabilidad del diseño FPGA depende de la pura suerte.
Opción nº4: Reset síncrono
El reset síncrono se conoce sobre todo por este patrón de código:
always @(posedge clk)
if (reset)
the_register <= 0;
else
[ ... ]
Más adelante sugeriré lo que considero un mejor patrón de código, pero por ahora nos quedamos con este. En cualquier caso, observa que he elegido un reset activo por nivel alto, que es la opción más habitual con los resets síncronos. Al menos, esa es mi impresión.
Un reset síncrono es mejor que el reset asíncrono en casi todos los aspectos, excepto en estos:
- La señal de reset síncrono puede alcanzar fan-outs enormes, y se aplican restricciones de temporización sobre todos sus caminos. Así que este tipo de reset puede dificultar el cumplimiento de las restricciones de temporización.
- El reset síncrono no se puede usar cuando el reloj no está activo.
- Algunas FPGAs tienen recursos dedicados para el encaminamiento global, que solo se pueden usar con resets asíncronos (no estoy seguro de que esto sea realmente así, pero como lo he visto mencionado para FPGAs de Intel, añado el comentario).
Como rara vez se usan FPGAs sin un reloj activo (al contrario que los ASICs, que tradicionalmente necesitan esto para el test), y no está claro si el problema de los recursos dedicados para el encaminamiento existe realmente, me centraré en el tema principal: el fan-out. Por suerte, esto es fácil de resolver.
También vale la pena mencionar que el mismo problema de fan-out afecta al reset asíncrono igualmente, si se usa el sincronizador que se sugirió antes. Así que los únicos que pueden decir de verdad que el fan-out es una desventaja del reset síncrono son quienes usan el reset asíncrono con los relojes apagados (es decir, con clock gating).
Lo primero que se le ocurre a uno para resolver un problema de fan-out es usar una restricción o atributo del sintetizador para limitar el fan-out. Sin embargo, esta es la opción menos recomendable, porque el sintetizador simplemente duplica el flip-flop cuando se alcanza el límite. Por tanto, ocurre a menudo que las salidas de los flip-flops duplicados van a módulos con propósitos completamente distintos, por lo que los destinos de esas salidas pueden estar repartidos por toda la FPGA. Eso da lugar a rutados largos y a un retardo de propagación (propagation delay) importante.
Una solución sencilla y eficaz es crear un reset local para cada parte importante de la lógica. Algo así:
module medium_sized_module (
input clk,
input reset,
input [15:0] in_data,
output [15:0] out_data
);
(* dont_touch = "true" *) reg local_reset;
reg the_register;
always @(posedge clk)
local_reset <= reset;
always @(posedge clk)
if (local_reset)
the_register <= 0;
else
[ ... ]
La idea es que @local_reset sea una copia local de @reset (retrasada un ciclo de reloj). Usar @local_reset en lugar de @reset desde este módulo hacia abajo mantiene el fan-out en un nivel razonable. Como se espera que los consumidores de este reset local estén fuertemente interconectados entre sí, lo más probable es que se coloquen en una determinada región de la FPGA. Por tanto, el reset local no tendrá que recorrer largas distancias por el entramado lógico.
Es importante evitar que el sintetizador elimine los registros de reset local para optimizar la lógica. El sintetizador suele hacer esto cuando hay registros con comportamiento idéntico, aunque pertenezcan a módulos distintos. En el ejemplo anterior se muestra el atributo de síntesis de Vivado, es decir, "dont_touch". Cada sintetizador tiene su propia forma de hacerlo (en Quartus, se consigue lo mismo con "dont_merge" como atributo de síntesis).
Para verificar que el sintetizador ha conservado realmente todos los registros, resulta útil dar a todos estos registros el mismo nombre (p. ej. local_reset, como se ha sugerido arriba) y buscar después registros con ese nombre en el diseño implementado.
Por supuesto, no hace falta crear un reset local para cada módulo. Como cifra aproximada, un fan-out de 50-100 es razonable para un reset local, en particular cuando llega a elementos lógicos dentro de una región física pequeña de la FPGA.
El tema de reducir el fan-out también se trata en el contexto del cierre de temporización (timing closure).
Más sobre los resets síncronos
Un mito común sobre los resets síncronos es que si el sintetizador encuentra un patrón de código Verilog que corresponde a un reset síncrono, conectará la señal de reset a la entrada de reset síncrono del flip-flop. Esto puede ser así, pero a menudo no lo será.
Esto es distinto del reset asíncrono, que debe conectarse a la entrada de reset asíncrono del flip-flop. De lo contrario, el reset no funcionará sin un reloj.
Los sintetizadores tienden a no dar ningún significado especial al patrón de código, sino a calcular la ecuación lógica que se deriva del código Verilog. Considera este ejemplo:
always @(posedge clk)
if (reset)
the_register <= 0;
else if (some_condition)
the_register <= !the_register;
else if (some_other_condition)
the_register <= 0;
Una forma de leer este código es que la sentencia always comienza con un patrón estándar que pide un reset síncrono, y luego viene una definición específica del comportamiento del registro. En consecuencia, uno podría haber esperado que @reset se conectara a la entrada de reset síncrono del flip-flop correspondiente, y también que la salida de una función lógica (implementada como una LUT) llegara a la entrada de datos del flip-flop.
En realidad, los sintetizadores suelen implementar el valor de @the_register en el siguiente flanco de reloj de la forma más concisa posible. Por ejemplo, la entrada de reset del flip-flop puede conectarse a una función lógica (es decir, una LUT) que implementa la expresión (reset || (some_other_condition && !some_condition) ).
Pero hay una posibilidad todavía más interesante: puede que la entrada de reset del flip-flop no se use en absoluto. En su lugar, solo se usa la entrada de datos, y la función lógica usa la señal @reset como una de sus entradas. Así que si @reset está en alto, la salida de la función lógica es cero. De este modo, @reset efectivamente lleva a @the_register a cero, pero no se trata de forma distinta a cualquier otra señal.
Así que, para reiterar: aunque el flip-flop tenga una entrada de reset síncrono, los sintetizadores tienden a no tratar de forma especial el patrón de código del reset síncrono, y tampoco tratan la señal de reset de forma distinta a cualquier otra señal. La entrada de reset del flip-flop se usa de la mejor manera para implementar el comportamiento que exige el código Verilog. Eso puede significar a veces conectar la entrada de reset directamente a la señal de reset, a veces a alguna función lógica que puede implicar a la señal de reset, y a veces no usar la entrada de reset en absoluto. El sintetizador hace lo que mejor le ayude a cumplir sus objetivos de rendimiento, y nada más.
Los usuarios del Vivado de Xilinx pueden tener un mejor control sobre este asunto con dos atributos de síntesis: DIRECT_RESET y EXTRACT_RESET.
Terminaré con otra desventaja de los resets asíncronos: en la mayoría de las FPGAs, un flip-flop solo tiene una entrada de reset/set. Esa entrada puede comportarse de forma síncrona o asíncrona. Si el reset es síncrono, el sintetizador puede encontrar trucos para usar esta entrada con el fin de emplear menos LUTs en el esfuerzo por cumplir el comportamiento requerido. Un atajo de este tipo es imposible cuando hay un reset asíncrono. Así que un reset asíncrono ata las manos del sintetizador y le obliga a malgastar más recursos lógicos.
Evitar la congelación accidental de registros
Hay una trampa con los patrones de código más usados para implementar resets, como se muestra en este ejemplo de código:
always @(posedge clk or negedge resetn)
if (!resetn)
begin
reg1 <= 0;
reg2 <= 0;
// Ayeee! Forgot to reset reg3 !
end
else
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
end
Como sugiere el comentario, @reg3 no aparece en la cláusula begin-end para cuando @resetn está activo. Como resultado, el código Verilog anterior exige que @reg3 no cambie de valor mientras @resetn esté activo. Es lo mismo que si @reg3 se definiera con
always @(posedge clk)
if (resetn)
reg3 <= [ ... ];
o, en otras palabras, @resetn funciona como una habilitación de reloj (clock enable) para @reg3: el reloj solo tiene efecto cuando @resetn está en alto.
Exactamente lo mismo ocurre con los resets síncronos:
always @(posedge clk)
if (reset)
begin
reg1 <= 0;
reg2 <= 0;
// Ayeee! Forgot to reset reg3 !
end
else
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
end
Esto es en realidad más fácil de entender, porque no es más que un par de cláusulas begin-end, y la segunda solo se aplica cuando @reset está inactivo. Así que la definición de @reg3 en este ejemplo es claramente
always @(posedge clk)
if (!reset)
reg3 <= [ ... ];
Así que la conclusión obvia (y no necesariamente inteligente) es no olvidarse de ningún registro en la cláusula begin-end del reset. De hecho, muchos diseñadores de FPGAs resetean todos los registros, lo necesiten o no, porque creen que esa es la única forma de hacerlo. O bien adoptan un estilo de código en el que cada registro tiene su propia sentencia "always".
Pero ¿y si quieres resetear a propósito unos registros y otros no?
Con un reset asíncrono, la única opción es poner esos registros en una sentencia "always" separada. Pero con un reset síncrono hay una forma sencilla de resolverlo:
always @(posedge clk)
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
if (reset)
begin
reg1 <= 0;
reg2 <= 0;
// I don't want to reset reg3, and that's fine!
end
end
En lugar de poner un "if (reset)" al principio y tener la parte interesante dentro de la cláusula "else", se pone el "if (reset)" al final, de modo que las asignaciones del reset anulan todo lo que vino antes.
Observa que esto no es equivalente a ninguno de los ejemplos anteriores: @reg1 y @reg2 se resetean cuando @reset está activo, pero @reg3 no se ve influenciado por el reset en absoluto.
Si te sientes incómodo con esta forma alternativa de aplicar un reset síncrono, te entiendo, y hay algunas razones para ello: para empezar, por lo general es una buena práctica ceñirse a los patrones de código habituales en el diseño con FPGAs. De lo contrario, el sintetizador puede mostrar algún error exótico, algo mucho menos probable con patrones de código bien asentados (ver Regla de Oro nº4). Así que aunque el estándar Verilog exige explícitamente que este método funcione, uno podría argumentar que no es necesariamente buena idea confiar en esta característica.
Es un argumento con peso, pero por si sirve de algo, estoy aquí para decirte que he usado patrones de código como este intensivamente durante más de una década, con una gran variedad de sintetizadores. En particular, así es como defino los resets síncronos en mi propio código. Nunca he tenido ni un solo problema con esto.
Otra posible razón para que no te guste este método es pensar que el sintetizador podría no captar la indicación de que se desea un reset síncrono, porque no es el patrón de código habitual. Sin embargo, como ya se ha mencionado antes, la mayoría de los sintetizadores no captan la indicación de todas formas, y consideran el reset síncrono como una definición más del comportamiento requerido de la lógica. Así que esta razón no tiene fundamento.
Así que si quieres creerme cuando te digo que es seguro, te ahorrarás más de un quebradero de cabeza.
¿Se puede hacer lo mismo con un reset asíncrono? Por ejemplo, ¿qué hará esto?
always @(posedge clk or negedge resetn)
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
if (!resetn)
reg1 <= 0;
end
Esto, por supuesto, es una desviación del patrón de código habitual del reset asíncrono. Una prueba anecdótica con el sintetizador de Vivado reveló que captaba la indirecta y asignaba un reset asíncrono a @reg1.
Pero el comportamiento que se exige para @reg2 no se puede conseguir en una FPGA: como está escrito arriba, significa que tanto @clk como @resetn son relojes, y que @reg2 muestrea un valor nuevo en sus flancos de subida y de bajada, respectivamente. Como no conozco ninguna FPGA que tenga flip-flops con dos entradas de reloj, no hay posibilidad de sintetizar la definición de @reg2.
El sintetizador de Vivado reaccionó a esto ignorando la parte de "negedge resetn", y creó un flip-flop que solo usa @clk como reloj. No había rastro de esta rareza en el resultado de la síntesis, y el sintetizador tampoco emitió ninguna advertencia ni se quejó. Y eso a pesar de que el sintetizador creó una lógica que no se comporta como define el código Verilog.
Así que, concretamente con el sintetizador de Vivado, el mismo patrón de código funciona de hecho incluso para un reset asíncrono, pero no debería uno fiarse de ello: el código Verilog debe decir lo que quieres que haga la lógica. De lo contrario, el sintetizador tiene todo el derecho a malinterpretarlo como prefiera.
Con esto termina la segunda página de esta serie sobre resets. En la siguiente página se repasan distintos aspectos de la puesta en marcha de la FPGA después del encendido y del reset externo.