Esta página pertenece a una serie de páginas sobre temporización (timing). Las páginas anteriores explicaron la teoría de los cálculos de temporización, mostraron cómo escribir varias restricciones de temporización (timing constraints) y analizaron los principios del cierre de temporización (timing closure). Esta página trata sobre las restricciones de temporización para caminos multicycle (multi-cycle paths).
Introducción
Lo primero que hay que saber sobre los caminos multicycle (multi-cycle paths) es que, en general, es una mala idea. Aunque esta página explica cómo usar las restricciones de caminos multicycle (multi-cycle paths), la conclusión debería ser evitar esta técnica por completo. En el mundo de las ASIC hay razones para usarla, pero en un diseño para FPGA suele ser mejor añadir un reloj adicional.
Dicho esto, veamos cuándo tiene sentido este tipo de excepción de temporización.
Considera este ejemplo de código Verilog:
reg foo, bar;
reg en, pre_en;
always @(posedge clk)
begin
pre_en <= !pre_en;
en <= pre_en;
if (en)
begin
foo <= !foo;
bar <= foo;
end
end
Observa que este ejemplo está incompleto: probablemente necesites añadir atributos de síntesis a @pre_en y a @en. De lo contrario, pueden ocurrir cosas inesperadas debido a las optimizaciones del sintetizador (synthesizer). Más sobre esto abajo.
@pre_en alterna entre '0' y '1' en cada ciclo de reloj. @en hace lo mismo, con un pequeño retardo.
La parte de "if (en)" hace que @en asuma el papel de habilitación de reloj (clock enable) para todo lo que hay entre "begin" y "end": cuando @en está bajo, no ocurre nada en esa parte del código Verilog. En otras palabras, @foo y @bar se comportan como si el flanco de reloj no existiera cuando @en está bajo.
En este ejemplo, @en está alto una vez de cada dos ciclos de reloj. Por tanto, @foo y @bar se comportan como si la frecuencia del reloj fuera la mitad de la real. Los requisitos de temporización pueden relajarse en consecuencia: los cálculos para tsetup pueden hacerse con un periodo de reloj que sea el doble.
En cuanto a thold, no hay ningún cambio: el cálculo de este requisito supone que el mismo flanco del reloj llega a ambos flip-flops. El periodo del reloj no tiene importancia, como ya se explicó en el ejemplo de un análisis de thold. Por tanto, la ilusión de un reloj más lento no supone ninguna diferencia respecto a thold.
Cuándo usar una habilitación de reloj
Solo hay dos buenas razones para usar una habilitación de reloj (clock enable):
- Cuando un diseño lógico que ya funciona bien está basado en una habilitación de reloj.
- Cuando hay escasez de recursos de reloj. Esta no es una situación habitual, porque las FPGA suelen tener más recursos de los necesarios para este fin.
No está claro si una habilitación de reloj mejora el consumo de potencia en una FPGA. Podría decirse que un reloj adicional desperdicia potencia. Pero la habilitación de reloj es también una señal con un fan-out alto. Por término medio, una habilitación de reloj cambia de valor tan a menudo como el reloj al que sustituye. Así que en términos de cambios de estado lógico (que es el principal factor del consumo), no hay diferencia.
Siéntete libre de saltarte esta página a menos que tengas una razón especial por la que debas usar caminos multicycle. Aunque tengas en el diseño una habilitación de reloj técnicamente adecuada, puede ser mejor no usar la excepción de temporización correspondiente. En particular, si las restricciones de temporización se cumplen sin dificultad sin esa excepción, no merece la pena arriesgarse a cometer un error.
La excepción de temporización
En relación con el código Verilog anterior, estas son las restricciones de camino multicycle para Vivado:
set en_regs [all_fanout -endpoints_only -only_cells -flat [get_nets en]] set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
Lo mismo para Quartus:
set en_regs [get_fanouts en] set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
La diferencia entre Vivado y Quartus está solo en la primera línea: el comando "all_fanout" se usa con Vivado, y "get_fanouts" con Quartus. Las restricciones son similares en otras herramientas que trabajan con SDC.
La primera línea encuentra todos los elementos síncronos (celdas) que son el destino de cualquier camino (path) que comienza en @en. Esta lista de objetos celda se guarda en $en_regs. Las dos líneas siguientes cambian los requisitos de temporización para los caminos que comienzan y terminan en celdas incluidas en esa lista.
Todo esto requiere muchas explicaciones. ¿Por qué escribí la definición de $en_regs de esa manera? ¿Por qué hay dos comandos set_multicycle_path? ¿Por qué pone "-hold" en el segundo comando, si he dicho que los requisitos de thold no se ven afectados por los caminos multicycle?
Empezaré por los comandos set_multicycle_path, porque es la parte más fácil de explicar.
Los comandos set_multicycle_path
Como se ha mostrado antes, los comandos son
set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
Para generalizar un poco el ejemplo anterior, consideremos también esto: si la habilitación de reloj estuviera activa una vez de cada ocho ciclos de reloj, el código Verilog habría sido:
reg en;
reg [2:0] pre_en;
always @(posedge clk)
begin
pre_en <= pre_en + 1;
en <= (pre_en == 0);
end
Observa que @en se comporta como un impulso (strobe), y está activo durante un solo ciclo de reloj en cada ocasión. No es el bit más significativo de un divisor de reloj.
Las restricciones multicycle para esta posibilidad serían:
set_multicycle_path -setup -from $en_regs -to $en_regs 8 set_multicycle_path -hold -from $en_regs -to $en_regs 7
Con estos dos ejemplos, está claro que el número del primer comando set_multicycle_path es simplemente la razón de división de la habilitación de reloj.
En cuanto al segundo comando, es la misma razón de división, pero menos uno. Así que siempre es N y N-1.
No hay mucho que explicar sobre el primer comando: si la habilitación de reloj está activa un ciclo de cada N, el retardo permitido se multiplica por N. Eso es relevante para el requisito de tsetup.
Pero, ¿por qué existe el segundo comando? ¿Por qué hace falta decir algo sobre thold? La respuesta es que el primer comando también cambia el requisito del retardo mínimo. En otras palabras, el cálculo para thold también se ve afectado por el comando con la opción "-setup". ¿Por qué? Probablemente no hay una buena explicación.
El segundo comando corrige esto: cambia de nuevo el requisito del retardo mínimo a su valor original. Así, después del segundo comando, el cálculo para thold se hace como antes.
En la documentación hay explicaciones extensas sobre por qué se usa N-1 en el segundo comando. Pero, siendo sinceros, no hay nada interesante en esa información adicional. El comportamiento de set_multicycle_path respecto a thold es extraño, y entender por qué el número debe ser N-1 no lo hace menos extraño.
Pero set_multicycle_path era la parte fácil. Ahora viene la verdadera dificultad: generar la lista correcta de objetos celda para usar como $en_regs.
Selección de los registros
Para seleccionar qué registros deben incluirse en $en_regs, es necesario entender qué hace que un camino sea candidato a camino multicycle. Esta es la regla: solo se permite relajar el requisito de temporización de un camino si la habilitación de reloj controla ambos lados. Esto significa que, cuando la habilitación está inactiva, está garantizado que ninguno de los dos elementos secuenciales cambia su valor después del flanco de reloj.
Piénsalo en términos de dominios de reloj (clock domains): todos los elementos secuenciales controlados por la habilitación de reloj pertenecen a un dominio de reloj imaginario. El reloj dentro de este dominio imaginario tiene una frecuencia menor, así que los requisitos de temporización dentro de este dominio pueden ajustarse.
Pero si uno de los lados del camino no pertenece a ese dominio de reloj imaginario, tenemos un cruce entre dos relojes relacionados (related clocks) de un dominio imaginario. No hay que hacer nada especial con ese camino, porque ya lo cubren las restricciones de temporización existentes. Pero es incorrecto aplicar una excepción multicycle a un camino de este tipo.
Las restricciones de temporización mostradas antes reflejan esta idea: todos los elementos secuenciales controlados por @en se incluyen en $en_regs como objetos celda. Después, los dos comandos set_multicycle_path se aplican a los caminos que comienzan y terminan en elementos secuenciales pertenecientes a esa lista.
La parte más difícil de los caminos multicycle es asegurarse de que la lista de elementos secuenciales sea correcta: debe contener todos los elementos secuenciales controlados por la habilitación de reloj, y ningún otro.
Si falta un elemento secuencial en la lista, la aplicación de los requisitos de temporización en los caminos correspondientes será más estricta de lo necesario. No es un desastre, pero hace menos eficiente la excepción de temporización.
Pero si se añade por error a la lista un elemento secuencial que no debería estar, puede haber consecuencias graves: habrá caminos cuyos requisitos de temporización no sean suficientemente estrictos. En otras palabras, las herramientas no garantizan los requisitos de funcionamiento correcto de esos elementos secuenciales. Y cuando no se cumplen los requisitos de temporización, pueden ocurrir cosas extrañas.
He elegido usar el comando "all_fanout" (o "get_fanouts") para crear esa lista de elementos secuenciales. No siempre está garantizado que funcione correctamente, y de eso voy a hablar a continuación. Después comentaré otras opciones para crear esta lista. Esas otras opciones son relevantes sobre todo cuando las herramientas de FPGA no soportan "all_fanout" ni comandos similares.
Posibles problemas con all_fanout y get_fanouts
El error más probable con una restricción multicycle es que la propia habilitación de reloj (es decir, @en) quede incluida en la lista (es decir, en $en_regs). Si eso ocurre, la excepción multicycle se aplica a todos los caminos que van desde @en hasta los elementos secuenciales que controla. Eso significa en la práctica que los requisitos de temporización de esos caminos no están garantizados. Esto puede tener un efecto visible, porque la habilitación de reloj suele ser una señal con un fan-out alto.
En el ejemplo anterior, esto se evita con @pre_en. Puede que te hayas preguntado por qué no se definió @en simplemente así:
always @(posedge clk)
en <= !en; // Wrong!
Si @en se hubiera definido así, habría un camino desde @en hasta sí mismo. Como resultado, @en habría quedado incluido en $en_regs.
Así que @pre_en resuelve este problema. Pero es importante asegurarse de que el sintetizador (synthesizer) no elimine este registro por optimización. Por ejemplo, el sintetizador de Quartus detecta que el único uso de @pre_en es dar un valor a @en (en el código Verilog de la parte superior de esta página). Por tanto, el sintetizador elimina @pre_en y continúa como si pusiera "en <= !en". Como resultado, @en queda incluido en $en_regs. Una posible solución es declarar @pre_en así:
reg pre_en /* synthesis preserve */;
Este ejemplo sencillo demuestra cómo una optimización inesperada del sintetizador puede tener un resultado desastroso. Aunque la solución es sencilla, es fácil pasar por alto la necesidad de impedir esa optimización.
Otro posible contratiempo con la habilitación de reloj está relacionado con el hecho de que esta señal suele tener un fan-out alto. Las herramientas pueden replicar automáticamente el registro, de modo que cada réplica tenga un fan-out inferior a cierto límite. Pero, ¿cómo afectará eso a $en_regs? El criterio para incluir un elemento en la lista se basaba en una red (net) concreta. Los elementos secuenciales controlados por réplicas de @en no quedarían incluidos.
El resultado de una situación así no es desastroso, sin embargo: como ya se ha dicho, solo significa que para algunos caminos la aplicación de los requisitos de temporización será más estricta de lo necesario. La fiabilidad del diseño no se ve afectada.
La replicación de registros ya se comentó en el contexto de los fan-outs altos. Como se dijo allí, es mejor replicar @en manualmente que esperar a que lo haga el sintetizador. Para evitar sorpresas, añade siempre un atributo de síntesis que impida la replicación de este registro. Si más adelante un fan-out alto causa problemas con el cierre de temporización (timing closure), resuélvelos con una replicación manual. Así será más fácil entender el origen del problema. Si el sintetizador replica @en de repente porque el proyecto ha crecido, no será tan fácil darse cuenta de por qué no se cumplen las restricciones.
En cualquier caso, el comando que define $en_regs debe actualizarse para que las réplicas de @en queden incluidas.
Hablando de lo cual, observa que la definición de $en_regs se apoya en el nombre de una red. Como ya se ha comentado, esto significa que $en_regs se convierte en una lista vacía si el sintetizador cambia el nombre de la red a algo distinto de "en". En consecuencia, las restricciones de camino multicycle quedan completamente inútiles. Esta posibilidad tampoco es un desastre: el diseño sigue siendo fiable, pero es más difícil cumplir las restricciones de temporización.
Otro problema posible es que @en no debe usarse para nada más que como habilitación de reloj, debido a la forma en que se define $en_regs. Por ejemplo, considera este código Verilog:
reg [7:0] counter;
always @(posedge clk)
if (en)
counter <= counter + 1;
En este ejemplo, @en se usa claramente como habilitación de reloj. Por tanto, es correcto que todos los caminos relacionados con @counter sean caminos multicycle. ¿Pero qué hay de esto?
reg [7:0] counter;
always @(posedge clk)
if (en)
counter <= counter + 1;
else
counter <= counter - 1;
Aquí, @en se usa como cualquier registro. El valor de @counter cambia en cada ciclo de reloj. Así que @counter no debería ser candidato a camino multicycle en absoluto. Y sin embargo, todos los flip-flops de @counter quedan incluidos en $en_regs: hay caminos desde @en hasta todos esos flip-flops.
Esto es relativamente fácil de resolver creando una réplica de @en:
reg [7:0] counter;
reg non_ce_en;
always @(posedge clk)
non_ce_en <= pre_en;
always @(posedge clk)
if (non_ce_en)
counter <= counter + 1;
else
counter <= counter + 2;
Observa que se necesita un atributo de síntesis para impedir que el sintetizador fusione @en y @non_ce_en en un solo registro.
En conclusión, la definición de $en_regs basada en todos los caminos que comienzan en @en es sencilla y concisa. Pero también es un campo de minas. Veamos algunas alternativas.
Formas alternativas de crear $en_regs
La forma más segura de crear una lista de elementos secuenciales para una excepción de camino multicycle es apoyarse en la jerarquía del diseño: toda la lógica controlada por la habilitación de reloj debería estar en un módulo aparte (y posiblemente en submódulos). Eso permite crear $en_regs buscando celdas por el nombre completo del objeto celda. Por ejemplo, con Vivado:
set all_sync [all_fanout -endpoints_only -only_cells -flat \ [get_nets -of_objects [get_clocks clk]]] set en_regs [filter $all_sync {name =~ module_ins/multicycle_ins/* }]
El primer comando encuentra todos los elementos lógicos conectados a @clk, excepto el propio buffer de reloj (este reloj está representado por el objeto de reloj llamado "clk"). El resultado se guarda en $all_sync. Esta es una forma posible de hacer una lista que incluya todos los elementos síncronos que puedan ser relevantes. El segundo comando crea una lista con todos los elementos lógicos de $all_sync que están dentro del mencionado módulo aparte.
Observa que este método se apoya en el nombre del objeto de reloj y en los nombres de las instanciaciones (instantiation). No se espera que esos nombres cambien. Con este método, no importa si la habilitación de reloj se replica o si el sintetizador le cambia el nombre.
Otra ventaja de usar un módulo aparte es que resulta más fácil trabajar con el código Verilog: hay menos probabilidad de confundir los elementos secuenciales controlados por la habilitación de reloj con los que no lo están.
Sin embargo, no siempre resulta natural separar en un módulo aparte la lógica que depende de la habilitación de reloj. Además, si el código Verilog ya está escrito y se sabe que funciona correctamente, puede no ser buena idea hacerle cambios.
Mencionaré también otra alternativa que puede ser adecuada en algunas situaciones: una convención de nombres para todos los registros. Por ejemplo, es posible dar a todos los registros controlados por la habilitación de reloj un nombre que empiece por "MC_". Esa elección hace sencillo el comando que crea $en_regs: basta con buscar objetos celda según su nombre. Otros elementos lógicos (por ejemplo, block RAM) pueden incluirse también eligiendo el nombre de su instanciación en consecuencia. Algunos dirán que este método hace más feo el código Verilog, y otros dirán que facilita el trabajo. Ningún método es perfecto.
Por qué no usar -of_objects
Puede resultar atractivo definir $en_regs con un criterio sencillo: encontrar la red que se llama "en" y añadir todos los registros conectados a ella. Por ejemplo, en Vivado esto puede escribirse así:
set en_regs [get_cells -of_objects [get_nets en]]
Hay varias razones por las que esto es incorrecto. La primera es que incluye al propio @en. Por tanto, las restricciones multicycle se aplican a todos los caminos que van desde la propia habilitación de reloj hasta los elementos secuenciales que controla. Como se ha dicho antes, esto es un error grave.
La segunda razón es que algunos elementos secuenciales pueden quedar fuera. Según el comando anterior, el criterio de inclusión es que la celda esté conectada a una red concreta (@en). Esto funciona cuando esa red está conectada directamente a la entrada CE del flip-flop. Pero a menudo el sintetizador prefiere usar una función combinacional basada en @en.
Por ejemplo, el sintetizador puede elegir implementar @foo como si el código Verilog fuera este:
foo <= foo ^ en;
Esto es funcionalmente equivalente a la expresión original:
if (en)
foo <= !foo;
Una optimización de este tipo es legítima y debe esperarse: el sintetizador debe usar una LUT de todos modos para implementar la puerta NOT. Entonces, ¿por qué no usar esa LUT para obtener directamente el siguiente valor del flip-flop? ¿Por qué añadir otro cable a la entrada CE del flip-flop?
El efecto secundario de esta optimización es que el propio flip-flop no está conectado directamente a @en. Por tanto, no quedará incluido en $en_regs. Las implicaciones de una situación así ya se han comentado.
A menudo es posible indicar al sintetizador que use @en solo como entrada de habilitación de reloj de los elementos síncronos. Por ejemplo, algunos sintetizadores soportan un atributo de síntesis llamado "direct_enable" o algo parecido. Observa que al usar esta funcionalidad, la libertad del sintetizador para optimizar la lógica se reduce. Así que el rendimiento del diseño puede verse afectado negativamente para resolver un problema técnico con las herramientas.
Y por si fuera poco, si @en se replica o se renombra, aparecen los mismos problemas mencionados antes.
Así que, por todas estas razones, usar como criterio la conexión directa a una red es una mala elección.
Interacción con un reset
Supongamos que añadimos un reset síncrono (synchronous reset) al ejemplo Verilog anterior:
always @(posedge clk)
begin
pre_en <= !pre_en;
en <= pre_en;
end
always @(posedge clk)
if (reset)
begin
foo <= 0;
bar <= 0;
end
else if (en)
begin
foo <= !foo;
bar <= foo;
end
Recuerda que la idea de una excepción multicycle era que todos los elementos síncronos se comportaran como si formaran parte de un dominio de reloj imaginario. El reloj imaginario dentro de ese dominio tiene la mitad de frecuencia que @clk. Por tanto, todos los registros deben ignorar @clk cuando @en está bajo. Esto no es cierto en este último ejemplo de código Verilog: @reset tiene efecto independientemente de @en.
Por ejemplo, considera qué ocurre si @reset se define así:
assign reset = foo;
Este es un reset síncrono legítimo, aunque probablemente no tenga ninguna utilidad práctica. Pero esta definición muestra el problema de una excepción multicycle: cuando @en está alto, @foo se pone alto en el siguiente ciclo de reloj. Pero eso hace que @reset también se ponga alto. Así que en el siguiente ciclo de reloj, @foo vuelve a ponerse bajo. @foo cambia de valor en cada ciclo de reloj. Por tanto, el camino multicycle de @foo a sí mismo impone un requisito de temporización que no es suficientemente estricto en ese camino.
Esto se resuelve fácilmente haciendo que @en controle también el reset síncrono:
always @(posedge clk)
if (en && reset)
begin
foo <= 0;
bar <= 0;
end
else if (en)
begin
foo <= !foo;
bar <= foo;
end
Esto es correcto; sin embargo, @reset debe estar activo junto con @en. Una solución sencilla es mantener @reset alto durante varios ciclos de reloj.
Los mismos principios se aplican a un reset asíncrono (asynchronous reset). Todo lo que se escribe en la página sobre resets asíncronos es relevante aquí también, pero con las restricciones multicycle es aún más complicado. La solución más fácil probablemente sea usar un sincronizador, como se sugiere en otra página.
@en y los resets asíncronos
Una de las ventajas de @pre_en es que garantiza que todas las réplicas de @en tengan siempre el mismo nivel lógico. Puede parecer obvio, pero no está garantizado si un reset asíncrono se usa incorrectamente. Por ejemplo, supongamos que el código Verilog original era:
reg en;
always @(posedge clk or posedge reset)
if (reset)
en <= 0;
else
en <= !en; // This is not recommended!
Si @en se replica, el resultado puede ser equivalente a esto:
reg en, en_1, en_2;
always @(posedge clk or posedge reset)
if (reset)
begin
en <= 0;
en_1 <= 0;
en_2 <= 0;
end
else
begin
en <= !en;
en_1 <= !en_1;
en_2 <= !en_2;
end
Observa que el siguiente valor de @en_1 depende de su propio valor, no del valor de @en. Este es un resultado realista de una replicación de un registro.
¿Qué ocurre si el reset asíncrono se desactiva de forma insegura? Es posible que algunas réplicas de @en reaccionen al primer flanco de reloj después del reset, y otras réplicas ignoren ese flanco. El resultado será que el estado lógico de las réplicas nunca llegará a ser el mismo (hasta el siguiente reset).
@pre_en resuelve esto, porque todas las réplicas copian su siguiente valor de la misma fuente. Eso garantiza un funcionamiento correcto a largo plazo, incluso si el arranque es brusco.
Resumen
Es fácil usar una habilitación de reloj. Es fácil usar set_multicycle_path como comando de excepción de temporización. Pero hacer que todo funcione de forma fiable no es nada fácil. Hay muchas cosas que pueden salir mal, y a veces la razón es que el sintetizador cambia su comportamiento cuando el diseño lógico crece.
El resultado de estos problemas inesperados puede ser que el camino multicycle no cumpla su propósito: si los requisitos relajados no se aplican en algunos caminos, el beneficio de este método es cuestionable. Peor aún, un error puede llevar a un diseño lógico poco fiable, si la excepción de camino multicycle se aplica a caminos que no deberían verse afectados.
Por tanto, es crucial leer los informes de temporización (timing reports) de los caminos relevantes para asegurarse de que no ha ocurrido nada inesperado. Desgraciadamente, eso no evita sorpresas en el futuro: el comportamiento del sintetizador es difícil de predecir a medida que el diseño evoluciona.
Así que, si es posible, es mucho mejor generar un reloj adicional con la misma PLL en lugar de usar una habilitación de reloj. Los cruces de dominios de reloj (clock domain crossing) con ese nuevo reloj son fiables, porque los dos relojes son relojes relacionados (related clocks). Las restricciones de temporización para ese nuevo reloj las generan automáticamente las herramientas. Así no hay riesgo de sorpresas.
Así que, si estás leyendo esto porque quieres añadir una habilitación de reloj y una excepción multicycle a tu diseño, espero que esta página te haya dado algo en lo que pensar.
Con esto concluye la parte sobre restricciones de temporización para los caminos internos de la FPGA. ¿Pero qué pasa con la E/S? Eso es lo que empieza a tratar la página siguiente.