01signal.com

Entender la reconfiguración parcial con Vivado

Esta es la primera entrada de una serie de cuatro sobre la reconfiguración parcial (partial reconfiguration), también llamada Dynamic Function eXchange (DFX), con Vivado de Xilinx. La intención de esta entrada es explicar los conceptos principales de este tema, lo que prepara el terreno para la siguiente entrada, en la que se describen los pasos prácticos para montar un proyecto FPGA con reconfiguración parcial.

Introducción

La reconfiguración parcial es una técnica que permite sustituir la lógica de algunas partes de la FPGA mientras el resto sigue funcionando con normalidad. Consiste en alimentar la FPGA con un flujo de bits (bitstream), exactamente igual que el flujo de bits inicial que programa su funcionalidad al encender. Sin embargo, el flujo de bits de la reconfiguración parcial no hace que la FPGA se detenga. En lugar de eso, actúa sobre elementos lógicos concretos y actualiza las celdas de memoria que controlan su comportamiento. Es una sustitución en caliente de bloques lógicos específicos.

Las FPGA de Xilinx soportan esta función desde Virtex-4 (y las FPGA de Intel desde la Serie V).

En esta entrada se repasan los conceptos que hay detrás de la reconfiguración parcial, sin entrar en los detalles técnicos prácticos, como preparación para la siguiente entrada, que hace exactamente eso. Como en este tema todo está relacionado con todo, es importante entender el marco completo antes de descomponerlo en acciones individuales.

¿Te lo explico otra vez?

Empecemos por cómo se cambia la funcionalidad de una FPGA sin reconfiguración parcial. Supongamos que hay una instanciación (instantiation) de un módulo en algún lugar de la jerarquía del diseño. Por ejemplo, en Verilog:

   moduleA reconfig_ins
     (
      .clk(clk),
      .this(this_w),
      .that(that_w),
 [ ... ]
      );

o en VHDL:

  reconfig_ins : moduleA
    port map(
      clk        => clk,
      this       => this_w,
      that       => that_w,
 [ ... ]
      );

Evidentemente, en algún lugar del proyecto hay un módulo llamado moduleA.v o moduleA.vhd, o una IP (IP core) llamada moduleA, que da contenido a la instanciación (junto con sus submódulos). Así que realizamos una implementación del proyecto, obtenemos un archivo de flujo de bits y cargamos la FPGA con él. Hasta aquí, el procedimiento habitual.

Pero ahora digamos que escribimos moduleB en lugar de moduleA en el código anterior, y realizamos una implementación de la lógica para obtener un flujo de bits. Para que esto funcione, tiene que haber un moduleB.v o moduleB.vhd, o una IP llamada moduleB en el diseño.

Ahora tenemos dos archivos de flujo de bits, que se diferencian en la lógica que hay dentro de la instancia llamada reconfig_ins. Para cambiar entre estos dos flujos de bits, tenemos que cargar la FPGA completa con el flujo de bits deseado. Esto implica una interrupción del funcionamiento de la FPGA.

La reconfiguración parcial es una técnica que permite pasar de una versión a otra sin esa interrupción: la FPGA sigue funcionando con normalidad, mientras la lógica en reconfig_ins cambia de moduleA a moduleB, y viceversa. Casi huelga decir que esto no es posible simplemente implementando los dos diseños por separado.

moduleA y moduleB se denominan módulos reconfigurables (RM), lo que significa que su lógica puede inyectarse en la FPGA mediante la reconfiguración parcial.

Motivación

Hay varias razones para usar la reconfiguración parcial, por ejemplo:

El flujo de bits parcial

Si tienes algo de experiencia con diseño de FPGA, es probable que estés acostumbrado a una rutina sencilla: haces algunos cambios en el código fuente del diseño (y en las IPs), lanzas las herramientas de implementación y compruebas que han terminado bien. Luego cargas el flujo de bits en la FPGA por JTAG. O, alternativamente, cargas un dispositivo flash con una imagen del flujo de bits.

Como es a lo que estamos todos acostumbrados, es fácil confundir el flujo de bits con un montón de datos que llena la FPGA de información mística sobre cómo debe comportarse cada elemento lógico. En realidad, un flujo de bits consiste en una serie de comandos que la FPGA ejecuta secuencialmente mientras se carga. Efectivamente, el flujo de bits habitual carga la FPGA entera con información, pero lo hace mediante varios comandos que controlan el avance del procedimiento. Un aspecto más importante de estos comandos es que determinan qué elementos lógicos reciben cada pieza de datos.

Como el propio flujo de bits indica qué elementos lógicos se ven afectados, es posible crear un flujo de bits que modifique solo algunos elementos lógicos y deje otros intactos. Esa es la piedra angular de la reconfiguración parcial.

Dicho esto, el flujo de bits parcial debe ser compatible con la lógica que ya está cargada en la FPGA. No es solo cuestión de no sobrescribir los elementos lógicos equivocados: el flujo de bits inicial está estrechamente acoplado con el parcial, en particular porque el inicial usa recursos de lógica y encaminamiento que están dentro del área que modifica el parcial. Cuando el flujo de bits parcial se configura correctamente respecto al inicial, este delicado baile pasa desapercibido. Si no, lo más probable es que la FPGA se vuelva loca, incluida la funcionalidad que debería haber permanecido intacta.

Carga del flujo de bits parcial

La entrega de un flujo de bits de configuración parcial a la FPGA se puede hacer con cualquier interfaz que permita cargar flujos de bits, siempre que el proceso pueda hacerse mientras la FPGA está en marcha. Esto incluye la interfaz JTAG, así que un archivo .bit de configuración parcial se puede cargar con el Hardware Manager como de costumbre. Pero aún más interesante: se puede hacer desde dentro de la propia lógica de la FPGA, usando el puerto dedicado de acceso a la configuración interna (Internal Configuration Access Port, ICAP). Este puerto solo se puede usar para la reconfiguración parcial, ya que la parte de la trama lógica de la FPGA que carga el flujo de bits debe permanecer intacta durante todo el proceso.

El ICAP no es más que una interfaz hacia el subsistema de la FPGA que carga flujos de bits, y no impone nada sobre el origen del flujo de bits. Por tanto, no hay ninguna limitación sobre cómo llegan los datos del flujo de bits a la FPGA, ni dónde se almacenan ni cómo. Solo tienen que estar disponibles de alguna manera para la parte de lógica de la FPGA que alimenta el ICAP.

Por ejemplo, Xillybus ofrece un medio sencillo para enviar un archivo de flujo de bits al ICAP desde un ordenador mediante una interfaz PCIe o USB 3.x, si la placa dispone de ella.

Lógica estática

Para hacer bien la reconfiguración parcial, hay que respetar la parte contraria: la lógica estática. Es un término general para las partes del diseño de FPGA que deben permanecer intactas y que, por tanto, están presentes desde que se cargó el flujo de bits inicial.

Esta lógica es estática en dos sentidos: el funcional, que significa que la lógica está formada por las partes del diseño de FPGA (HDL e IP) que funcionarán sin interrupciones desde el arranque inicial de la FPGA. El segundo aspecto, no menos importante, es que la ubicación de esta lógica se limita a emplazamientos de la trama lógica asignados como estáticos. En esos emplazamientos no se permite ninguna manipulación posterior.

En un diseño real no basta con que la lógica estática permanezca sin cambios: también es importante que siga funcionando correctamente mientras se cambian otras partes de la FPGA. Como casi con seguridad hay redes que conectan la lógica estática con la que cambia, es responsabilidad del diseñador de FPGA asegurarse de que todo vaya sobre ruedas. La tercera entrada de esta serie habla de ello.

Separación entre lógica estática y lógica reconfigurable

Para que la reconfiguración parcial sea siquiera posible, tiene que haber una separación estricta entre la lógica estática y la lógica reconfigurable. En particular, los elementos lógicos físicos de la FPGA deben estar separados, de modo que ningún emplazamiento que contenga lógica estática se vea afectado cuando se carga la FPGA con el flujo de bits.

Para entender qué implica esto, veamos primero a lo que estamos todos acostumbrados.

Recuerda que el proceso habitual de implementación de FPGA empieza con una síntesis del diseño HDL. Observa que la instanciación de módulos en HDL no implica ninguna separación entre ellos. Más bien al contrario: el sintetizador (synthesizer) trata las instanciaciones como una descripción de cómo debe funcionar la lógica. Por consiguiente, el sintetizador es libre de considerar todo el diseño como una única pieza de lógica grande y plana. Las optimizaciones que cruzan las fronteras de los módulos no solo están permitidas, sino que son deseables y ocurren mucho. Por ejemplo, si un registro del módulo X resulta ser equivalente a un registro completamente no relacionado del módulo Y, se elimina uno de los registros y el que queda se usa en ambos módulos (a menos que se le diga explícitamente al sintetizador que se abstenga de hacerlo).

Una vez completada la síntesis del HDL, la lista de conexiones (netlist) sintetizada se mezcla con la de las IPs del diseño (si las hay).

A continuación, este gran bloque de elementos lógicos se coloca por toda la trama lógica de la FPGA, y los cables se encaminan de la manera que cumple las restricciones de temporización (timing constraints) y otros objetivos. La lógica de distintas partes del diseño puede terminar empaquetada en el mismo slice o en extremos opuestos de la FPGA. Incluso el cambio más pequeño en el diseño puede provocar una colocación drásticamente distinta. Es caótico pero inofensivo, ya que cada implementación es independiente, y a quién le importa cómo se reparte la lógica por la trama de la FPGA.

Volvamos a la reconfiguración parcial: como acabamos de mencionar, para que esta función sea siquiera posible, tiene que haber una distinción clara entre lógica estática y lógica reconfigurable. Para garantizarlo se usa una técnica llamada diseño jerárquico. La idea es considerar todo el diseño como una colección de componentes, como los componentes físicos de una PCB. Por un lado, a cada componente (es decir, módulo instanciado) se le asigna una cierta zona de la trama lógica. Y como cada componente necesita estar separado, tiene todo el sentido realizar la síntesis por separado, igual que fabricarías el componente por separado.

Conectemos este concepto con la reconfiguración parcial, que se reduce a dos diferencias principales en el trabajo con el diseño:

Pblocks

La terminología de Vivado para una unidad de planificación física es Pblock, que no es más que un contenedor de información dentro de Vivado. Hay funciones Tcl para crear un Pblock, añadirle celdas lógicas y luego añadir grupos (conjuntos) de emplazamientos lógicos de la FPGA. Vivado lo interpreta como una restricción de ubicación que exige que las celdas lógicas añadidas al Pblock solo puedan colocarse en los emplazamientos que se le hayan asignado. Así que, al final, los Pblocks son simplemente como otras restricciones del archivo XDC.

Los Pblocks se suelen definir con la interfaz gráfica de Vivado: se abre el diseño sintetizado o implementado y se dibujan regiones rectangulares sobre la representación gráfica de la FPGA. Eso crea un Pblock que incluye todos los elementos lógicos del rectángulo dibujado. Más concretamente, no se incluyen todos los tipos de elementos lógicos, sino solo aquellos para los que está permitida la planificación física (para esa familia de FPGA). Así, Vivado traduce el rectángulo en rangos de elementos lógicos.

Es igualmente válido establecer estos rangos manualmente editando el archivo XDC. También se permite crear una región compuesta por varios rectángulos, así que la forma puede ser más compleja que un único rectángulo. No obstante, la documentación de Xilinx (UG909) sugiere intentar mantener las formas simples para evitar dificultades con el encaminamiento.

Este es un ejemplo de archivo XDC para Kintex-7:

create_pblock pblock_pr_block_ins
add_cells_to_pblock [get_pblocks pblock_pr_block_ins] [get_cells -quiet [list pr_block_ins]]
resize_pblock [get_pblocks pblock_pr_block_ins] -add {SLICE_X118Y0:SLICE_X153Y99 SLICE_X118Y250:SLICE_X145Y349 SLICE_X0Y0:SLICE_X117Y349}
resize_pblock [get_pblocks pblock_pr_block_ins] -add {DSP48_X5Y100:DSP48_X5Y139 DSP48_X5Y0:DSP48_X5Y39 DSP48_X0Y0:DSP48_X4Y139}
resize_pblock [get_pblocks pblock_pr_block_ins] -add {RAMB18_X4Y0:RAMB18_X6Y39 RAMB18_X4Y100:RAMB18_X5Y139 RAMB18_X0Y0:RAMB18_X3Y139}
resize_pblock [get_pblocks pblock_pr_block_ins] -add {RAMB36_X4Y0:RAMB36_X6Y19 RAMB36_X4Y50:RAMB36_X5Y69 RAMB36_X0Y0:RAMB36_X3Y69}

La imagen siguiente muestra cómo se ve en el diseño implementado. La partición reconfigurable (llamada pblock_pr_block_ins, que en este ejemplo casi no contiene lógica) está dibujada en morado. Su forma se crea como unión de tres rectángulos (los tres rangos indicados en cada comando resize_pblock de arriba).

En este dibujo, toda la lógica colocada aparece en cian. La gran mayoría de la lógica pertenece a la región estática, y se aprecia claramente que está confinada en una zona pequeña.

Example of device view with Pblock partition

Observa que solo los slices, las RAMs y los DSP48 están limitados por restricciones. Son los únicos tipos de lógica que la reconfiguración parcial puede controlar en las FPGA de la serie 7. Todo lo demás —que en la práctica es todo lo que no sea "lógica pura"— debe pertenecer al diseño estático.

Con las FPGA Ultrascale y posteriores, prácticamente cualquier lógica puede recargarse mediante reconfiguración parcial.

Hay restricciones adicionales sobre los Pblocks, pero no tiene sentido repetir aquí los capítulos 6 a 8 de UG909.

En cualquier caso, es buena idea abrir un diseño implementado, hagas lo que hagas, y hacer zoom dentro y fuera de la vista de la FPGA, y observar cómo se organizan los elementos lógicos en la FPGA. En particular, observa que hay columnas de lógica del mismo tipo. A veces hay algunos elementos lógicos en medio de las columnas que rompen la uniformidad (en particular elementos lógicos especiales, como el bloque ICAP, los bloques PCIe, etc.).

Vale la pena mencionar que el tema de los Pblocks no es exclusivo de la reconfiguración parcial. Por ejemplo, todo lo dicho en esta sección también se aplica al uso de Pblocks para diseño jerárquico.

Más sobre la planificación física

Y aquí vienen un par de hechos contraintuitivos: aunque la representación gráfica de la planificación física consiste en formas dibujadas sobre el mapa de la FPGA, esta solo se aplica a los tipos de lógica que están controlados por sus restricciones de ubicación. Así que si casi todos los slices de la FPGA están asignados a la reconfiguración parcial, puede ocurrir perfectamente que islas de otros elementos lógicos completamente rodeadas por esos slices pertenezcan a la lógica estática. Por ejemplo, no hay ningún problema si el propio bloque ICAP está en medio de un rectángulo asignado a la reconfiguración parcial.

Esto no es tan raro, dado que el flujo de bits de reconfiguración parcial se dirige a ciertos elementos lógicos y deja otros intactos. Pero, ¿y el encaminamiento? Si un bloque ICAP está empotrado en medio de lógica reconfigurable, ¿cómo se llevan los cables hasta los slices de la lógica estática?

Esto nos lleva al segundo hecho contraintuitivo: el encaminamiento del diseño estático usa recursos dentro de la región reconfigurable. Ese encaminamiento permanece estable durante todo el proceso de reconfiguración parcial, porque si no, no sería lógica estática. Así que, dentro de la región reconfigurable, el encaminamiento de la lógica reconfigurable cambia, pero el de la lógica estática permanece igual. Si hay algo que parezca magia en todo este tema, es este pequeño detalle. También es la razón por la que usar un flujo de bits parcial que no sea compatible con la lógica estática probablemente altere la FPGA por completo.

Lo contrario no es cierto, por supuesto: la lógica reconfigurable no usa ningún recurso salvo los que se indican explícitamente en el ejemplo XDC de arriba. En cuanto a los recursos de encaminamiento, nada en la región estática cambiará mientras se carga un flujo de bits de reconfiguración parcial, así que en ese sentido la lógica reconfigurable nunca influye en la región estática. Bueno, eso es casi cierto: si la forma de la región reconfigurable no es un rectángulo simple, Vivado puede permitir que el encaminamiento salga de la región reconfigurable. Esto ocurre solo en las FPGA Ultrascale, con el fin de mejorar el encaminamiento.

Lo que debería quedar claro a estas alturas es que las reglas para dibujar la planificación física no son simples. La buena noticia es que Vivado produce avisos críticos (critical warnings) bastante informativos cuando se violan las reglas de la planificación física. Por tanto, encontrar la distribución física adecuada por prueba y error es una forma razonable de trabajar.

Implementaciones padre e hijas

En cuanto a la implementación de la lógica reconfigurable, es importante tener en cuenta que todos y cada uno de los caminos (path) de la FPGA deben cumplir las restricciones de temporización, y eso debe ser cierto antes y después de cargar la lógica reconfigurable. Por tanto, una implementación de la lógica reconfigurable separada de la lógica estática es imposible. Más bien, la implementación se realiza siempre sobre la FPGA completa para cada módulo reconfigurable. Las restricciones de temporización (y las demás restricciones) se imponen en cada implementación.

Para aclarar este punto, volvamos al ejemplo de moduleA y moduleB de arriba. En este ejemplo, Vivado realiza la implementación del diseño completo con moduleA incluido, y luego hace lo mismo con moduleB. Como subproducto, se obtienen flujos de bits normales para cada una de estas dos opciones.

Vale la pena enfatizarlo: todas las implementaciones producen un flujo de bits inicial completo y también un flujo de bits parcial. Esto es cierto para todas las implementaciones, sean padre o hija. Por tanto, cuando la FPGA se enciende, es posible cargarla con cualquiera de esos flujos de bits iniciales.

Para que sea posible pasar de moduleA a moduleB con reconfiguración parcial, todo en la partición estática debe ser exactamente igual. Esto incluye la propia lógica, la ubicación y el encaminamiento. Para lograrlo, Vivado realiza la implementación de un escenario (por ejemplo, con moduleA) como implementación padre. Luego realiza implementaciones hijas para todos los demás escenarios (por ejemplo, con moduleB). Cómo se hace exactamente está detallado en la última entrada de esta serie, pero para resumir:

Vivado empieza ejecutando la implementación padre para moduleA como es habitual en un diseño jerárquico. Esto significa que la síntesis de la lógica estática y de la reconfigurable se hace por separado, y que las restricciones de planificación física fuerzan la ubicación en emplazamientos separados de la FPGA. Aparte de estas dos diferencias, se realiza una implementación normal. En particular, la colocación y el encaminamiento se realizan para obtener resultados óptimos en este escenario concreto (aunque las restricciones de planificación física y la síntesis separada puedan dar lugar a un rendimiento subóptimo).

El siguiente paso es llevar a cabo la implementación hija para moduleB. No hace falta sintetizar el diseño estático, porque ya se hizo en la implementación padre. Así que solo se sintetiza la lógica reconfigurable.

A continuación, la implementación se realiza igual que la implementación padre, con una diferencia crucial: la colocación y encaminamiento (place and route) de toda la lógica estática se fuerza a ser idéntica al resultado de la implementación padre. Dada esta limitación, la colocación y el encaminamiento de la lógica reconfigurable se hacen buscando resultados óptimos.

Así que la clave de la relación entre padre e hija es que una implementación hija empieza donde terminó la implementación padre, pero sustituye la lógica reconfigurable por su propia lógica. La implementación hija continúa entonces como siempre, pero sin tocar nada en el área de la lógica estática.

Como todas las implementaciones hijas tienen que adaptarse a las ubicaciones y encaminamientos de la lógica estática, puede resultar más difícil cumplir las restricciones de temporización que en una implementación normal del diseño. De hecho, hay dos obstáculos:

Esto hay que tenerlo en cuenta al elegir cuál de los módulos reconfigurables se usa para la implementación padre. Por ejemplo, puede ser el módulo con el que sea más difícil cumplir las restricciones de temporización. O el módulo que representa a los demás en la forma de conectarse con la lógica estática. O quizás al revés: un módulo reconfigurable que no incluya lógica efectivamente (una "caja gris"), para conseguir una implementación neutra de la lógica estática.

En cuanto al uso de los flujos de bits, el paradigma de Vivado para la implementación de un proyecto con reconfiguración parcial es que la implementación termina cuando todos los flujos de bits están actualizados y son mutuamente compatibles. En otras palabras, cualquiera de los flujos de bits iniciales de las implementaciones puede usarse para cargar la FPGA al principio. Después, se puede cargar cualquiera de los flujos de bits parciales de las implementaciones.

Por eso, el primer "Generate Bitstream" arranca la implementación padre y todas las implementaciones hijas. En las compilaciones siguientes, Vivado solo ejecuta aquellas que necesitan actualizarse, como es habitual.

El asistente Dynamic Function eXchange

El propósito de este asistente, que se puede iniciar desde el menú Tools, es definir la implementación padre y las implementaciones hijas y, en particular, qué implementación contiene cada módulo reconfigurable.

Es más fácil explicar este asistente mirando los comandos Tcl que genera cuando se añade una implementación hija:

create_reconfig_module -name bpf -partition_def [get_partition_defs pr ]
add_files -norecurse /path/to/pr_block1.v  -of_objects [get_reconfig_modules bpf]
create_pr_configuration -name config_2 -partitions [list pr_block_ins:bpf ]
create_run child_0_impl_1 -parent_run impl_1 -pr_config config_2 -flow {Vivado Implementation 2020}

Voy a repasar esta secuencia Tcl al revés, de la última línea a la primera:

En la última línea se crea una ejecución de implementación hija. A la nueva ejecución se le da el nombre "child_0_impl_1" y su ejecución padre se elige como "impl_1". No menos importante, la configuración de esta nueva ejecución se establece como "config_2".

"config_2" se define en la tercera línea, indicando que "bpf" es el módulo reconfigurable destinado a la partición reconfigurable llamada "pr_block_ins". "pr_block_ins" ya ha aparecido antes, pero ¿qué es "bpf"?

En la primera línea se crea un reconfig_module llamado "bpf": es solo un nombre cualquiera que sirve para identificar qué hace esa lógica. La segunda línea indica que se añade un archivo Verilog concreto a este módulo reconfigurable.

En resumen, estas cuatro líneas crean una nueva implementación hija y dicen que se necesita sintetizar un archivo Verilog concreto para crear un módulo reconfigurable. Además, se crean dos objetos en el entorno Tcl: "bpf" y "config_2".

Volvamos al asistente Dynamic Function eXchange: es una herramienta gráfica que representa las relaciones entre las fuentes del diseño, los módulos reconfigurables, las configuraciones y las ejecuciones de implementación. Es solo una forma cómoda de comunicar la información necesaria para generar comandos Tcl como los mostrados arriba.

Esta herramienta puede parecer excesivamente complicada, pero es porque el ejemplo es simple. En un diseño realista, es probable que reconfig_module tenga varios archivos fuente e incluso IPs asignadas. Así que la interfaz gráfica facilita las cosas.

Pero, ¿por qué es necesaria la configuración ("config_2")? ¿Por qué no se hace la conexión entre "bpf" y "pr_block_ins" con el comando create_run? Una vez más, es una pregunta legítima, porque esta entrada se limita a una sola partición reconfigurable. Si hay varias particiones de este tipo, una configuración define qué partición recibe cada módulo reconfigurable, así que tiene sentido dar a cada combinación un nombre, como config_*.

Entonces, si hay varias particiones, ¿es obligatorio hacer una implementación para cada combinación posible de módulos reconfigurables? Esta pregunta no es relevante para esta serie de entradas, así que siéntete libre de saltar a la siguiente sección.

Recuerda que la implementación de Vivado se hace sobre todo el diseño, es decir, la lógica estática y la reconfigurable juntas, y garantiza que cumpla las restricciones de temporización en su conjunto. Por tanto, si hay varias particiones, la forma segura de usar la reconfiguración parcial es cargar todas las particiones con sus flujos de bits parciales, de modo que todos los flujos de bits parciales sean el resultado de la misma ejecución de implementación. En otras palabras, todos los flujos de bits parciales se crearon con la misma configuración (por ejemplo, "config_2"), y por tanto la combinación de estos flujos de bits parciales es el resultado de una implementación que las herramientas han validado. En particular, se sabe que esta implementación cumple las restricciones de temporización.

Y sin embargo, si los módulos reconfigurables no interactúan entre sí (es decir, todos los puertos de nivel superior de los módulos reconfigurables están conectados a la lógica estática, no unos con otros), no puedo imaginar qué podría fallar si se trata cada partición por separado. Ciertamente, Vivado no ha aprobado explícitamente la temporización del conjunto de la FPGA si se mezclan flujos de bits parciales de distintas ejecuciones. Pero puesto que todos los caminos con la lógica estática cumplen las restricciones de temporización, y la lógica estática es exactamente la misma en todas las ejecuciones de implementación, ¿no es suficiente? La documentación oficial no parece dar información sobre este asunto.

Encaminamiento y pines de partición

Aún falta una pieza del rompecabezas: el encaminamiento que conecta la lógica estática con la lógica reconfigurable. Recuerda que la implementación padre hace la colocación y encaminamiento del diseño de forma óptima para la lógica reconfigurable incluida en la configuración correspondiente. Pero el módulo reconfigurable de la hija debe encajar en la misma partición reconfigurable y conectarse con el diseño estático. Al menos una parte del encaminamiento pertenece a la lógica estática y, por tanto, no puede cambiar.

Aquí es donde entran en juego los pines de partición (partition pins). Conceptualmente, se puede pensar en la lógica reconfigurable como un componente físico, y en los pines de partición como las patillas metálicas que conectan con la PCB.

Sin embargo, en realidad los pines de partición son solo posiciones en el sistema de coordenadas de los recursos de encaminamiento de la FPGA. Son los lugares donde termina el encaminamiento de la lógica estática y continúa el de la lógica reconfigurable. Su única importancia es que la implementación padre y las implementaciones hijas se pongan de acuerdo sobre dónde están.

Para establecer estos puntos de anclaje no se necesitan recursos físicos como LUTs ni biestables (flip-flops), y no añaden retardo de encaminamiento adicional. El tramo de encaminamiento que va hacia los pines de partición y sale de ellos crea un retardo, por supuesto, pero los propios pines de partición no añaden ningún retardo.

Las posiciones de los pines de partición las eligen automáticamente las herramientas durante la implementación padre, y las implementaciones hijas se ven obligadas a adaptarse. En otras palabras, el encaminamiento entre la lógica estática y la reconfigurable empieza donde dijo la implementación padre, y la implementación hija solo puede hacer lo que pueda dentro de la partición reconfigurable. Puede resultar que algunos pines de partición estén en posiciones desfavorables para la lógica reconfigurable de la hija, lo que podría dificultar el cumplimiento de las restricciones de temporización.

Los pines de partición suelen agruparse cerca del perímetro de la partición reconfigurable. Parece que Vivado está diseñado para elegir emplazamientos que no estén demasiado especializados para un diseño concreto.

Sin embargo, los pines de partición pueden encontrarse en cualquier lugar dentro de la partición reconfigurable, si eso fue necesario para cumplir las restricciones de temporización durante la implementación padre. Recuerda que al diseño estático se le permite usar recursos de encaminamiento que están dentro de la partición reconfigurable. Por tanto, no hay ningún problema en que parte del encaminamiento estático se adentre en la partición reconfigurable.

Para evitar problemas con las restricciones de temporización y los pines de partición, conviene que los puertos de salida del módulo reconfigurable sean registros, y que las entradas también sean muestreadas por registros. Del mismo modo, a la lógica estática le conviene aplicar registros de forma parecida. De hecho, siempre es buena idea seguir esta regla, siempre que sea posible y no complique el diseño.

El greybox

Una cosa más sobre el asistente DFX es el greybox: en la ventana Edit Configuration es posible asignar un greybox como módulo reconfigurable, en lugar de uno de los módulos reconfigurables normales. Un greybox es un módulo ficticio que genera Vivado. Encaja con los puertos del módulo reconfigurable real, pero en lugar de lógica real tiene un LUT por cada pin de puerto. Los LUT que se generan para las entradas no están conectados a nada en el otro extremo, y los LUT para las salidas producen un valor cero. Para puertos vectoriales, se crea un LUT por cada bit del vector.

Puede no ser buena idea usar un greybox en la implementación padre, porque se lo pone demasiado fácil al proceso de colocación y encaminamiento. Aunque los módulos reconfigurables sean muy distintos entre sí, probablemente sea mejor escribir un módulo simple que suponga un cierto reto para las herramientas.

Pero para crear un flujo de bits inicial con lógica mínima, puede ser útil una implementación hija que contenga solo módulos greybox. Recuerda que todas las implementaciones producen flujos de bits completos, y todos pueden usarse como flujo de bits inicial, ya que todos tienen exactamente la misma lógica estática.

Flujos de bits de borrado (solo Ultrascale)

Esto se refiere solo a FPGA Ultrascale (no Ultrascale+).

Como se ha mencionado antes, todas las implementaciones crean dos flujos de bits: uno para el diseño completo, que se puede usar para cargar la FPGA inicialmente, con el módulo reconfigurable correspondiente incluido. El segundo flujo de bits es para la reconfiguración parcial con el mismo módulo reconfigurable.

Con los dispositivos Ultrascale hay un tercer flujo de bits, el llamado flujo de bits de borrado (clearing bitstream), que se crea en cada implementación. Este flujo de bits debe enviarse a la FPGA antes que el parcial. Observa que el flujo de bits de borrado enviado a la FPGA debe corresponderse con la lógica que está actualmente dentro de la FPGA, no con el flujo de bits que se va a cargar. Por tanto, hay que llevar la cuenta de la situación actual de la FPGA, algo que no es necesario con otras familias de FPGA.

Cargar el flujo de bits de borrado apaga el módulo reconfigurable, aunque en realidad no cambia la lógica. Los puertos de salida de este módulo pueden mostrar cualquier valor hasta que se haya cargado y puesto en marcha un nuevo flujo de bits parcial.

Según UG909, cargar el flujo de bits de borrado del módulo reconfigurable equivocado (es decir, no el que corresponde a la lógica que ya está en la FPGA) puede alterar también la lógica estática y, por tanto, causar un mal funcionamiento del mecanismo de reconfiguración.

La documentación de Xilinx parece ser vaga sobre lo que ocurre si se carga un flujo de bits parcial sin cargar antes el de borrado. En el capítulo 9 de UG909 dice primero: "Antes de cargar un flujo de bits parcial para un nuevo módulo reconfigurable, el módulo reconfigurable existente debe borrarse". Así que la conclusión sería que el flujo de bits de borrado es obligatorio.

Pero unas líneas más abajo, la misma guía dice: "Si no se carga un archivo de flujo de bits de borrado, las rutinas de inicialización (GSR) no tienen efecto". Esto implica que se puede prescindir por completo del flujo de bits de borrado, si se acepta que todos los elementos síncronos (RAMs y biestables, en principio) se despierten en un estado desconocido. En mis propios experimentos anecdóticos saltándome el flujo de bits de borrado no vi problemas, pero eso no demuestra nada.

Así que con las FPGA Ultrascale, sin duda hay que llevar la cuenta de qué hay cargado en la FPGA. Esto se puede hacer, por ejemplo, añadiendo un puerto de salida al módulo reconfigurable que tenga un valor constante (un código ID) distinto para cada módulo reconfigurable. Esto permite que la lógica estática identifique qué módulo reconfigurable está cargado en cada momento. Un código ID así puede ser una buena idea en cualquier caso.

Casos de uso: plugin frente a actualización remota

Esta metodología padre-hija, que Vivado ha adoptado, está aparentemente pensada para un caso de uso concreto, que llamaré uso tipo plugin: reducir el coste de la FPGA cargando solo el módulo reconfigurable que se necesita en cada momento, en lugar de tener todas las funcionalidades posibles dentro de la FPGA permanentemente. Por ejemplo, si la FPGA se usa para implementar varios filtros de imagen, la reconfiguración parcial permite implementar cada filtro como módulo reconfigurable, y recargar la FPGA solo con el filtro que se necesite.

Xilinx usa el término "Dynamic Function eXchange" (DFX) para la configuración parcial, lo que parece reflejar el uso principal previsto para esta técnica.

El método padre-hija funciona bien cuando ese es el propósito de la reconfiguración parcial. Se genera un kit completo de archivos de flujo de bits. Cualquiera de los flujos de bits iniciales puede usarse para inicializar la FPGA, y todos los flujos de bits reconfigurables pueden usarse después para la reconfiguración parcial. Cuando se publica una nueva versión del proyecto, se sustituye el kit completo, que consiste en todos los archivos de bits.

Pero hay otro patrón de uso, al que llamaré actualización remota (Remote Update). Es cuando la reconfiguración parcial se utiliza como medio para actualizar versiones, quizás en un futuro lejano. En este escenario, el flujo de bits inicial se publica en un momento dado y no se puede cambiar después. Más adelante se publican flujos de bits parciales, y estos deben ser compatibles con el flujo de bits inicial. Estas publicaciones posteriores pueden prolongarse durante varios años.

Para la actualización remota, el método padre-hija puede ser difícil de usar tal cual. Aunque es posible ejecutar la implementación hija para obtener un nuevo flujo de bits parcial sin repetir la implementación padre, puede resultar difícil mantenerlo durante mucho tiempo. Por ejemplo, un cambio accidental en el código fuente de la lógica estática invalida el diseño de la padre, lo que provoca una nueva implementación padre. Como resultado, la nueva lógica estática es incompatible con la anterior y, por tanto, los flujos de bits parciales basados en ella no se pueden usar con el flujo de bits inicial original.

Por tanto, si la reconfiguración parcial se concibe como un método para actualizar sucesivamente el diseño de la FPGA a lo largo del tiempo, el procedimiento de implementación necesita cierta manipulación. Este tema se trata en la última entrada de esta serie.

Compresión de flujos de bits

Esto no está directamente relacionado con la reconfiguración parcial, pero a veces se desea tener un archivo de flujo de bits inicial pequeño, en particular para asegurar un arranque rápido de la FPGA. En este contexto, la reconfiguración parcial se convierte en un medio para completar el proceso de puesta en marcha después del arranque rápido. Esto se puede hacer desde la misma fuente de datos (por ejemplo, la flash SPI) o desde una completamente distinta (por ejemplo, una interfaz PCIe).

Comprimir el flujo de bits está permitido tanto para el flujo de bits inicial como para los parciales.

Esta es la línea que hay que añadir al archivo XDC para solicitar un flujo de bits comprimido:

set_property bitstream.general.compress true [current_design]

Con esto concluye la parte teórica. La siguiente entrada muestra los pasos prácticos para configurar un proyecto y usar la reconfiguración parcial.

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)