Esta página es la tercera de una serie de cinco sobre cómo convertirse en diseñador profesional de FPGA. En esta página intentaré esbozar las habilidades prácticas más importantes que se necesitan para ser diseñador de FPGA. También intentaré aclarar por qué es importante cada habilidad. No te saltes la parte de Verilog, aunque parezca obvia: puede que tenga unas cuantas cosas no tan obvias que decir al respecto.
Y antes de empezar, explicaré por qué solo menciono Verilog y no VHDL: simplemente porque se recomienda Verilog, ya que parece que va a ser el lenguaje HDL dominante en el futuro. Esto se debe, entre otras cosas, a que en China apenas se oye hablar de VHDL. Pero todo lo que digo a continuación también es válido para VHDL.
Las dos caras de Verilog
Por qué es necesario saber Verilog apenas necesita explicación. De hecho, mucha gente por ahí piensa que saber Verilog equivale a ser diseñador de FPGA. Uno o dos jefes de proyecto han enviado a sus programadores junior a un curso de Verilog y se han llevado un chasco cuando no han vuelto convertidos en campeones de FPGA.
Así que, en primer lugar, conocer la sintaxis de Verilog está muy lejos de saber usar este lenguaje. Esto es cierto para cualquier lenguaje, pero con Verilog la brecha es mucho mayor. En segundo lugar, es importante entender que el código Verilog no es un programa de ordenador. Describe hardware. Por lo tanto, en principio, todo ocurre en paralelo. Este es el cambio mental que a muchos desarrolladores de software les cuesta asimilar, en particular a quienes no están acostumbrados a la programación multihilo.
La cuestión de si es o no un lenguaje de programación es, en realidad, algo más complicada, porque Verilog se usa con dos propósitos distintos: la simulación de hardware y la creación de lógica para una FPGA o un ASIC (síntesis).
El propósito de una simulación suele ser comprobar el código Verilog antes de sintetizarlo como lógica en la FPGA. Para ello escribimos un banco de pruebas (testbench), que en esencia es un programa de ordenador que crea artificialmente señales de estímulo y hace algo con las señales de salida procedentes del fragmento de lógica que estamos probando: normalmente escribir los valores de las señales en un archivo o comprobar que son los esperados.
Cuando Verilog se usa para simulación, en realidad sí es un lenguaje de programación, aunque no se parece en nada a C, Python o JavaScript. Aun así, el ordenador hace exactamente lo que le hemos pedido: cuando se ejecuta la simulación, tanto el banco de pruebas como el código que pretendemos sintetizar se comportan exactamente como define la sintaxis.
Y aquí llega la gran trampa: cuando se usa para síntesis el mismo código Verilog que probamos, Verilog ya no es un lenguaje de programación. Describe lógica, igual que HTML describe lo que debe mostrarse en una página web. Y lo que es peor, y a diferencia de HTML, la interpretación del código Verilog puede ser bastante difusa.
Es importante entender que sintetizar código Verilog es una especie de magia. Nosotros, los humanos, describimos el comportamiento que queremos, y el sintetizador (synthesizer) lo implementa de alguna manera con la ayuda de recursos lógicos. Por eso tenemos que definir ese comportamiento con patrones de codificación concretos que el sintetizador reconoce y para los que genera la lógica que pretendíamos. A diferencia de la programación normal, no podemos escribir simplemente lo que nos apetezca. Tenemos que pensar en los elementos lógicos que generará el sintetizador como resultado de nuestras peticiones. El código Verilog no son más que pistas para el sintetizador sobre qué lógica generar.
En la era de la IA, también tengo que añadir algo: Verilog no es programar a lo loco. Es un lenguaje formal, y el sintetizador no es un LLM al que convenzamos con buenas palabras para que haga lo que queremos. Los sintetizadores existían mucho antes de que los LLM fueran algo, e interpretan nuestro código según reglas estrictas, y no según una red neuronal entrenada.
Y lo más importante de todo: si escribimos mal el código Verilog, la lógica no se comportará como en la simulación. El simulador hará exactamente lo que queremos, porque el simulador hace lo que el código dice. El sintetizador, en cambio, puede generar silenciosamente una lógica que haga algo distinto, si no tenemos cuidado de escribir el código Verilog correctamente.
Este punto es crucial y merece la pena repetirlo: cuando Verilog se usa para simulación, es un lenguaje de programación, aunque sea bastante flojo. Lo único que tienes que hacer es acertar con la sintaxis, y el simulador hace exactamente lo que quieres. El sintetizador, en cambio, puede generar una lógica completamente distinta de la simulación, incluso aunque la sintaxis de Verilog sea perfectamente correcta. Si tienes suerte, el sintetizador emitirá un aviso (warning) de que puede que no obtengas lo que esperabas. Con suerte, verás ese aviso, a pesar de estar enterrado entre cientos de otros. Si tienes aún más suerte, el sintetizador se detendrá con un error. Pero con demasiada frecuencia obtienes un fallo silencioso. Para evitarlo, tienes que saber qué patrones de codificación son válidos para síntesis. Y no intentar nada más.
Y ahora llega la pregunta: ¿cómo se aprende Verilog como es debido?
Habilidades básicas de Verilog
El primer paso es aprender la sintaxis, por supuesto. Hay un montón de libros y tutoriales de Verilog por ahí. Lamentablemente, muchísimos de ellos lo repasan absolutamente todo, lo cual no solo es innecesario, sino que puede tentarte a usar patrones de codificación que los sintetizadores estropearán.
Te sugiero el siguiente conjunto mínimo de temas. Apréndelos de cualquier fuente que encuentres, y con eso ya estarás listo en lo que a sintaxis se refiere. Si te topas con algo que no conoces en algún código Verilog con el que te cruces, pregúntale a tu IA favorita qué significa. No es más difícil que eso. Aquí tienes mi lista de la compra:
- Módulos e instancias, puertos (input, output, output reg, inout).
- Wires y registros. Registros que crean biestables y los que no (con always @(*) y similares).
- Asignaciones con "assign" frente a con always @(posedge clk) frente a always @(*). '=' frente a '<=' dentro de bloques always.
- Sentencias if
- Sentencias case y su uso para máquinas de estados (state machines), multiplexores y ROMs.
- Constantes en Verilog, por ejemplo 16'h1234 y 4'b1001. Entender también x y z como valores.
- Operaciones aritméticas y lógicas en Verilog. Entender la diferencia entre & y &&, por ejemplo. Operadores unarios de reducción, por ejemplo &myreg. Verdadero/falso como valor lógico, por ejemplo myreg <= (this == 1).
- Manipulaciones de bits: selección de bits (por ejemplo myreg[3:2]), así como concatenación (por ejemplo { myreg1, myreg2 }) y duplicación (por ejemplo { 8{myreg} } ).
- Parámetros de módulo (parameter y localparam).
- Usar IP e instanciarlos en tu módulo Verilog. Esta no es una habilidad puramente de Verilog, ya que implica usar la herramienta de desarrollo para crear y configurar un bloque IP (un FIFO o un PLL, por ejemplo). Y aun así, la mayor parte del trabajo consiste en instanciar y conectar los puertos en tu diseño, y eso sí es una habilidad de Verilog.
- El bloque "initial" para inicializar registros al encender, así como para asignar variables en simulación.
- Comandos que se usan solo para simulación: $stop, $finish, $display, $readmemh, y el uso extensivo de "initial".
- Retardos de tiempo con "#" y su uso en simulaciones.
- La directiva `timescale (y su falta de importancia en muchos casos).
- Sentencias generate — menos importantes para empezar, pero muy usadas.
- Atributos de síntesis. Son instrucciones directas al sintetizador (synthesizer), y su sintaxis varía de un fabricante a otro. Por ejemplo, DONT_TOUCH = "TRUE" se usa a menudo para indicar al sintetizador que no optimice y elimine un registro, aunque eso ahorraría lógica. Así que asegúrate de leer la lista de atributos de este tipo que admite tu sintetizador, porque pueden ser muy útiles.
Hasta aquí, la parte fácil. El verdadero problema es conseguir que el sintetizador genere la lógica que realmente queremos escribiendo el código Verilog correctamente. En realidad, conseguir que cualquier sintetizador del mundo genere exactamente esa misma lógica.
Yo me guío por un principio sencillo, que es la regla n.º 4 de mi propia lista de Reglas de Oro: asegúrate de usar patrones de codificación muy comunes. La idea es que, si el sintetizador malinterpreta mi código, cometerá el mismo error con el código de muchas otras personas también. Y eso no pasará, porque toda esa gente cambiaría pronto a otro sintetizador. Así que miro el código que he escrito y me pregunto: "¿a cuánta otra gente afectaría si el sintetizador estropeara esto?". Si la respuesta es "a mucha", probablemente esté del lado seguro. Insisto en el "probablemente".
Pero entonces, ¿cómo escribe Verilog toda esa otra gente? Eso es en realidad un hueso duro de roer, porque la gran mayoría del Verilog se escribe dentro de empresas y nunca se publica. Y cada persona escribe con un estilo de codificación distinto. Para empeorarlo, los ejemplos de Internet suelen estar escritos por gente sin experiencia. Aunque su código funcione, no es necesariamente algo que imitar: otro sintetizador podría estropear ese mismo código, posiblemente porque los trucos de síntesis se llevan al límite. El código Verilog de OpenCores varía en calidad desde excelente hasta código que solo funciona en simulación.
Entonces, ¿cómo se sabe? La mejor sugerencia que se me ocurre es leer el código Verilog que generan las herramientas de IP de AMD. Algunos de estos IP, por ejemplo los controladores de memoria DDR, generan Verilog sintetizable, y lo escribe gente que sabe lo que hace. Sin embargo, ten en cuenta que el código Verilog generado automáticamente a menudo contiene mucho código inútil y sin usar. Esto es típico del código generado por scripts. A menudo hay módulos que solo instancian otros módulos, que a su vez instancian otros módulos, y así sucesivamente. Envolver módulos indefinidamente de esta forma no es algo que imitar; es solo una manera de mantener la jerarquía estructurada según sea necesario para permitir distintas configuraciones del mismo código. El código Verilog generado automáticamente también tiende a tener muchos parámetros, lo cual tampoco hay que imitar necesariamente. Recuerda que tu objetivo es imitar su forma de expresar funcionalidad, no la jerarquía desordenada.
Una palabra sobre SystemVerilog: es bastante popular, pero yo personalmente nunca he escrito código en este dialecto del lenguaje. Esto se debe principalmente a que quiero que mi Verilog sea lo más sencillo posible. Cuanto menos tenga que confiar en el sintetizador, mejor. Cuando necesito estructuras complicadas, escribo un script en Perl que genera Verilog sencillo. Dale de comer al sintetizador con una cucharilla, o te lo devolverá todo.
Herramientas de implementación
Dominar esta habilidad significa trabajar de forma eficiente con las herramientas, tener un buen control sobre ellas, entender sus mensajes de aviso, permitir una generación de flujo de bits (bitstream) reproducible y mantener el proyecto manejable a medida que crece. En resumen, controlar tu trabajo en el proyecto.
Vivado es la herramienta preferida para trabajar. No solo domina AMD el mercado, sino que otros fabricantes, en particular los nuevos fabricantes chinos de FPGA, tienden a diseñar sus herramientas de forma compatible. Aparte de saber usar estas herramientas como IDE, hay que entender el proceso de convertir el código Verilog y los IP en un flujo de bits (bitstream). Empieza con la síntesis y va seguido de varios pasos más que dependen del fabricante, pero que en principio todos hacen lo mismo: transformar gradualmente la salida del sintetizador (synthesizer) en un flujo de bits que puedas cargar en la FPGA. Esta secuencia de pasos de ejecución no es una caja negra, y no debería tratarse como tal.
El principal motivo para entender cómo funcionan las herramientas es responder correctamente a los errores y avisos. Durante la implementación de un proyecto se generan muchísimos avisos, y es importante distinguir cuáles son importantes y cuáles se pueden ignorar. Y si se produce un error, el proceso falla y hay que resolver un problema. La habilidad para resolver este tipo de problemas viene de entender la teoría que hay detrás de las acciones de las herramientas, así como de la experiencia acumulada.
Intentar preguntarle a la IA cómo resolver un problema funciona a veces, pero a menudo la IA te lleva por un largo viaje de depuración inútil. Y si escuchas a la IA sin ningún criterio propio, puedes acabar haciendo algo que aparentemente resuelve el problema, cuando en realidad solo te has librado de un mensaje de error y has creado un problema real en su lugar. En resumen: no hay sustituto para tu propio cerebro, y nunca lo habrá.
Otro punto importante es que cada herramienta tiene sus peculiaridades, por ejemplo mensajes de error engañosos. O incluso peor: las herramientas ignoran silenciosamente código, ajustes o restricciones. Aprender estas cosas es solo cuestión de experiencia, y parte de adquirir esa experiencia consiste en leer de verdad los informes y averiguar qué significan esos mensajes, aunque no tengan una relevancia concreta.
Esta es una lista de conceptos y rutinas que te sugiero como lista de comprobación. Solo tienen que ver con la síntesis y la obtención de un flujo de bits (bitstream). Dejo la simulación, la verificación y la depuración para más adelante.
- Síntesis y generación de listas de red (netlists) (edif en particular)
- Mapeo tecnológico (technology mapping) (aunque en Vivado esto lo hace el sintetizador (synthesizer)).
- Inclusión de IP en el diseño.
- Emplazamiento y rutado (place and route)
- Optimizaciones de temporización posteriores al rutado (post-route)
- Generación del flujo de bits (bitstream)
- Uso de JTAG para cargar el flujo de bits o programar la memoria flash
- Visualización y análisis del diseño sintetizado y/o emplazado y rutado con las herramientas de Vivado.
Si pruebas un proyecto de ejemplo, es muy probable que pases por todo lo mencionado en esta lista, excepto por el último punto. Usar las herramientas cuando alguien ya te ha preparado un proyecto de ejemplo es fácil. En la vida real, los proyectos rara vez están organizados de forma tan ordenada, y tú serás el responsable de hacer que las herramientas funcionen correctamente y de obtener los mejores resultados posibles. Si no entiendes cómo funciona la maquinaria, te costará arreglarla cuando se atasque o no haga lo que quieres. Recuerda que pasan cosas raras todo el tiempo, incluso cuando lo haces todo bien.
Un océano de archivos
Las herramientas de desarrollo crean archivos, y muchos. Cada paso que ejecutan las herramientas, desde la síntesis hasta el flujo de bits (bitstream) finalizado, es como un programa de ordenador que lee unos archivos y produce sus resultados como otros archivos.
Para complicarlo aún más, las herramientas a menudo crean copias de los archivos fuente y se apoyan en esas copias en lugar de en los originales. Las herramientas también generan archivos intermedios y se apoyan en ellos en lugar de en algo que se parezca a un archivo fuente. Ten esto presente, en particular cuando hagas cambios en las fuentes y no cambie nada en los resultados.
No pondría como primer paso del aprendizaje entender qué hace cada archivo y para qué sirve. No tiene sentido intentar dominarlos todos, pero sí es importante saber qué archivos deben considerarse "fuente" y cuáles son "generados". Cuanto mejor nades en este océano de archivos, mejores serán tus posibilidades de mantener la cabeza fuera del agua. Entenderás a qué me refiero la primera vez que intentes crear una copia independiente de un proyecto entero, con el propósito de desarrollarlo en una dirección distinta. O de trasladarlo a otro ordenador.
El mejor método es mantener un conjunto mínimo de archivos que definan el proyecto de FPGA en un repositorio Git. Borra todos los demás archivos de vez en cuando y reconstruye el proyecto a partir de ese conjunto mínimo. Para ver un ejemplo de cómo se puede arrancar un proyecto desde un conjunto mínimo de archivos, descarga e intenta implementar uno de los paquetes de demostración (demo bundle) de Xillybus (disponibles tanto para Vivado como para Quartus). Ten en cuenta que no importas las fuentes normalmente en Vivado para empezar, sino que ejecutas un script de Tcl. Puede sonar como una solución que da miedo, pero ese script es fácil de modificar incluso si no sabes nada de Tcl.
Una cosa que merece la pena saber sobre Vivado es que el DCP es un archivo comprimido que contiene listas de red (netlists) en edif, restricciones y otra información. Por ejemplo, cuando el DCP es el resultado del emplazamiento y rutado (place and route) o de etapas posteriores, contiene las posiciones exactas de los elementos lógicos. Es una instantánea del diseño tras completar una etapa concreta del proceso. Te sugiero descomprimir un DCP y echarle un vistazo, simplemente por curiosidad.
Verificación y simulación
En un mundo perfecto, el diseño lógico funciona al primer intento y no hay fallos que corregir. La realidad casi siempre es distinta, por supuesto.
En cualquier curso para principiantes sobre Verilog te dirán esto: primero, escribe el código Verilog que quieres como lógica en la FPGA. A esto lo llamamos "código para síntesis" o "código sintetizable". Después, escribe un banco de pruebas (testbench) en Verilog y úsalo en una simulación para poder comprobar que el código para síntesis funciona correctamente. O, más bien, ver cómo no funciona correctamente y corregir los fallos. El banco de pruebas es código Verilog escrito solo con el propósito de simular, y que nunca se acerca a un sintetizador (synthesizer).
En efecto, el método de trabajo habitual es escribir un módulo Verilog, o unos cuantos módulos Verilog, y después escribir un banco de pruebas para verificar que funcionan correctamente. Esto se repite a medida que el proyecto crece y, al final, es posible que se simule todo el proyecto. Para una simulación tan grande suele haber varios bancos de pruebas distintos, cada uno destinado a comprobar funcionalidades diferentes.
Pero no todas las simulaciones se hacen de la misma manera. Hay tres enfoques principales para simular.
El primer enfoque es simular con el fin de obtener formas de onda. En este enfoque, el banco de pruebas solo crea señales que alimentan las entradas del módulo que queremos simular. Esto incluye los relojes y los resets, así como otras señales que imitan el comportamiento de señales físicas reales o de señales generadas por otros módulos. Una vez completada la simulación, se usa una interfaz gráfica (GUI) para ver las formas de onda creadas por la lógica bajo prueba, con el fin de comprobar si funciona correctamente o por qué no lo hace. Este método es adecuado para lógica sencilla. Por ejemplo, la máquina de estados (state machine) que implementa un patrón de salida de vídeo se puede simular así, ya que el patrón de salida repetido correcto se verifica fácilmente con solo mirar las formas de onda.
El segundo enfoque es leer la entrada desde archivos y escribir la salida en archivos. Aquí, el banco de pruebas crea unas cuantas señales sencillas, pero las señales importantes se leen de un archivo. Los valores de las salidas, o de unas cuantas salidas seleccionadas, del módulo probado se escriben en otro archivo. Es bastante común escribir un programa de ordenador o un script que genere el archivo que lee el banco de pruebas, así como el archivo que contiene la salida esperada del banco de pruebas. Después de ejecutar la simulación, se puede hacer una simple comparación de texto (diff) entre la salida esperada y lo que el banco de pruebas escribió realmente. Por supuesto, hay infinitas variantes de esto: el banco de pruebas puede hacer la comparación con las salidas esperadas o, posiblemente, al revés: un software de ordenador lee la salida del banco de pruebas y la analiza.
Este método de simulación es especialmente adecuado para lo que yo llamaba "lógica de procesado", es decir, lógica que implementa algún tipo de procesado de datos. También es útil para las pruebas de regresión, es decir, simulaciones que comprueban que nada ha cambiado funcionalmente de una versión del código a otra.
El tercer enfoque es dejar que el banco de pruebas compruebe la corrección por sí mismo y se detenga con un error si ocurre algo malo. Este método requiere escribir patrones de prueba significativos en Verilog, lo cual a menudo es más difícil que hacerlo en un lenguaje de scripting. Por otro lado, el proyecto de FPGA es más fácil de mantener cuando hay un banco de pruebas autocontenido que da una indicación de aprobado o suspenso. Quien ejecute la suite de regresión estará encantado de trabajar así.
Estos son los tres enfoques principales. He hecho que parezca que solo se simula el Verilog que escribimos los humanos, pero eso no es cierto:
Simulaciones posteriores a la síntesis
Como he mencionado antes, el sintetizador (synthesizer) puede producir una lógica distinta del comportamiento descrito por el código Verilog. Una forma de abordar este problema es simular la salida (es decir, la lista de red (netlist)) que produce el sintetizador. Esto se llama simulación posterior a la síntesis (post-synthesis simulation).
Para ejecutar una simulación posterior a la síntesis, pedimos a las herramientas de desarrollo que creen un modelo Verilog del código sintetizado. Es un módulo Verilog enorme que tiene los mismos puertos que el que escribimos para síntesis. Pero por dentro está formado por módulos pequeños (llamados primitivas de simulación (simulation primitives)) que representan los elementos lógicos reales de la FPGA a la que apuntamos. Así que es un archivo Verilog realmente desordenado, pero podemos referirnos a él en el banco de pruebas en lugar del módulo Verilog original que escribimos.
El método clásico es ejecutar una simulación sobre el código Verilog escrito por humanos y después sobre el modelo posterior a la síntesis, y comparar las salidas. El segundo método mencionado antes (con archivos como entrada y salida) es el mejor, porque esperamos que el diseño lógico se comporte exactamente igual después de la síntesis. Si no lo hace, considéralo un golpe enorme a tu estilo de codificación en Verilog. En realidad, si escribes Verilog correctamente, no deberías necesitar una simulación posterior a la síntesis. Este tipo de simulación es más habitual en la industria de los ASIC, donde son paranoicos con acabar con un fallo en su chip final. También merece la pena señalar que, aunque tu simulación posterior a la síntesis dé exactamente la misma salida que la original, eso sigue sin garantizar que el sintetizador haya hecho lo que esperabas.
También existe la simulación posterior al emplazamiento y rutado (post-place-and-route simulation). Como su nombre indica, simula los elementos lógicos tal como están emplazados y conectados dentro de la FPGA. Tiene en cuenta los retardos de propagación (propagation delay) dentro de la FPGA, pero de forma muy limitada. Sigue habiendo una diferencia enorme entre la simulación y lo que ocurre en la vida real. Esto se debe a que los retardos reales dentro de la FPGA dependen de muchos factores físicos, por ejemplo la temperatura y las tensiones de alimentación. Estos retardos también son algo aleatorios, debido a las contaminaciones del silicio del chip, que están dispersas por todas partes. Estas contaminaciones cambian las propiedades físicas, así que los retardos eléctricos se desvían ligeramente. Cada FPGA física tiene por tanto retardos distintos, aunque dentro de especificación. Cuando se ejecuta la simulación, solo se aplica un retardo concreto a cada camino (path), que normalmente es el mayor retardo permitido. Si tu diseño funciona perfectamente en una simulación posterior al emplazamiento y rutado, eso sigue sin significar que vaya a funcionar en una FPGA física real.
Así que, en conjunto, las simulaciones tienen limitaciones bastante serias. Por un lado, la simulación obedece tus deseos allí donde el sintetizador (synthesizer) no lo hará. Y ni siquiera las simulaciones posteriores a la síntesis pueden cubrir muchos efectos de la vida real dentro de la FPGA: glitches, tolerancias de los elementos lógicos físicos debidas a la temperatura, la tensión de alimentación o las tolerancias de fabricación. La simulación tampoco puede reproducir el comportamiento de la lógica como resultado de violaciones de temporización, por ejemplo al cruzar dominios de reloj (clock domains) de forma insegura.
Si el diseño está escrito y restringido correctamente, la simulación y la realidad se comportan igual. De lo contrario, la simulación no vale nada. Es importante tenerlo presente.
Otra limitación es que la simulación solo puede cubrir un segmento muy corto de tiempo. Si la simulación se ejecuta durante un millón de ciclos de reloj, lo cual puede llevar mucho tiempo, eso solo cubre 10 ms de tiempo real cuando el reloj va a 100 MHz. Los problemas raros y los casos límite (corner cases) se pasan por alto con facilidad, simplemente porque no cayeron dentro de la ventana simulada.
Mi sugerencia sobre las simulaciones
¿Qué recomiendo para un novato? Saber simular es imprescindible, y es algo que hay que aprender junto con las demás habilidades relacionadas con Verilog. En particular, asegúrate de manejar con soltura el segundo método de simulación, usando archivos para la entrada y la salida. Es el método que se considera serio, entre otras cosas porque es habitual en las pruebas de regresión. Pruébalo también con simulaciones posteriores a la síntesis y posteriores al emplazamiento y rutado (place and route), al menos por el bien de una entrevista de trabajo.
Pero: no te vuelvas adicto a las simulaciones. Date un capón cada vez que encuentres un fallo con una simulación. Si encuentras un fallo con el primer método (mirando formas de onda de una simulación), date un capón aún más fuerte. El objetivo final es escribir código que funcione al primer intento. Las simulaciones pillan los fallos sencillos, no los que causan algo raro cada mil millones de ciclos de reloj. Esos son los que no querrás perseguir eternamente.
Por último, un pequeño secreto sobre mí: rara vez ejecuto ninguna simulación. Me aseguro de escribir el código Verilog con cuidado y con reflexión, para que funcione correctamente de inmediato, o al menos con pocos fallos suficientes para corregirlos directamente en la FPGA. Pero no conozco a nadie más que trabaje así, y no estoy seguro de que lo recomendaría como enfoque general.
En realidad, me gustaría añadir un consejo extra avanzado, para tenerlo en cuenta más adelante: cuando la gente ejecuta simulaciones, a menudo añade resets a su lógica, porque en una simulación todos los registros empiezan en el estado desconocido, marcado con "X". Como resultado, no sale nada valioso, ya que todo el sistema permanece en el estado "X". El error habitual es poner rápidamente resets asíncronos (asynchronous resets) por todas partes para deshacerse de esas X. Y luego olvidar que eso puede ser una idea muy mala, como se explica en una página aparte. Así que sí, resuelve definitivamente el problema de las X, pero hazlo con criterio: ¿quizá con un reset síncrono (synchronous reset) o quizá con un bloque "initial" en el código sintetizable? La mayoría de los sintetizadores implementan este bloque, aunque originalmente solo esté pensado para simulación. No dejes que la simulación controle cómo escribes el código.
Pruebas y depuración
Después de todo lo dicho y hecho, acabarás probando tu diseño y encontrando los motivos por los que no funciona como esperabas. Hay un dicho que dice que la depuración es como una novela detectivesca, en la que la misma persona es la víctima, el detective y el culpable.
Y la verdad es que en esta novela detectivesca no hay una única forma óptima de resolver el misterio. Se trata de ser creativo a la hora de encontrar el enfoque, las herramientas y los métodos correctos cada vez, para ir acercándote a la fuente del fallo. Algunas personas se aferran siempre a los mismos métodos de depuración. A veces encuentran el problema bastante rápido y a veces tardan una eternidad.
Es bastante común desarrollar un conjunto de herramientas y métodos de depuración especializados para un proyecto concreto. Esto también ocurre en grandes proyectos de software, pero en el diseño de FPGA a menudo es inevitable. Es como desarrollar un banco de pruebas (testbench) para probar, pero en hardware.
Pero aunque no exista un único método óptimo para encontrar un fallo, sí hay ciertas herramientas que se usan habitualmente. Mencionaré algunas.
La herramienta con la que la mayoría prefiere empezar es un analizador lógico en chip (on-chip logic analyzer). Según la suite de desarrollo que tengas, esta herramienta se llama ILA, ChipScope, SignalTap o algo similar, y todas hacen lo mismo: convierten tu ordenador en un analizador lógico. Con esta herramienta puedes ver formas de onda capturadas dentro de la FPGA igual que ves las formas de onda de las simulaciones. Tú eliges qué señales observar y la condición para disparar una captura de esas señales.
La comunicación entre el ordenador y la FPGA se realiza por la interfaz JTAG, que es la misma que se usa para enviar el archivo del flujo de bits (bitstream) a la FPGA. Así que no se requiere hardware adicional, y muchos diseñadores de FPGA a los que no les gusta la electrónica agradecen este hecho. La FPGA y su enlace JTAG ya existente son también la herramienta de depuración.
Este método es tan cómodo que muchos diseñadores de FPGA se vuelven adictos a él y se olvidan de otros métodos que pueden ser más adecuados en algunas situaciones.
El principal inconveniente de este método es que JTAG tiene un ancho de banda de datos relativamente bajo, así que ILA y herramientas similares no son adecuadas cuando se requieren grandes cantidades de datos para la investigación. Además, los datos tardan un poco en llegar a través del enlace JTAG. Esto puede ser problemático cuando queremos una respuesta inmediata a los eventos, para poder correlacionarlos con otras cosas que ocurren.
Cuando las limitaciones del enlace JTAG se convierten en un problema, se necesitan interfaces más rápidas. Si la placa de FPGA tiene una interfaz PCIe, se puede usar para transportar datos a alta velocidad desde y hacia un ordenador. Implementar una interfaz PCIe y el controlador del ordenador puede ser un proyecto en sí mismo, pero con Xillybus es rápido y fácil poner en marcha un enlace así. También es posible una solución similar con Xillybus con placas Zynq-7000 que funcionan con Xillinux.
A menudo se requiere una conexión de alto ancho de banda cuando hay un problema muy concreto escondido entre una gran cantidad de datos. Por ejemplo, en el procesado de imágenes, en cuyo caso la imagen se transmite al ordenador a través de Xillybus y se visualiza en el monitor del ordenador con la ayuda de un programa de ordenador específico.
Ya elijas un analizador lógico en chip o Xillybus, son herramientas de depuración estériles. No se toca ningún hardware. Pero a veces necesitamos esa respuesta sencilla e inmediata del hardware para averiguar qué está pasando.
Así que, en primer lugar, permíteme que te sugiera la herramienta de depuración más sencilla de todas: el LED. Sorprendentemente a menudo, el método más rápido para encontrar un fallo es definir distintas condiciones lógicas y asegurarte de que un LED parpadea cuando se cumplen. En particular, que los LEDs parpadeen cuando ocurre algo que nunca debería ocurrir. Cuantos más LEDs, mejor. Esto se parece un poco a insertar un "printf" en el código C para depurar. Del mismo modo, añade un poco de código Verilog para depurar. Carga el flujo de bits (bitstream) en la FPGA, prueba algunas cosas absurdas al azar y puede que esos LEDs revelen con qué está relacionado el fallo.
Hay una cosa importante que recordar cuando uses este método: si se cumple la condición, asegúrate de que el LED permanezca encendido al menos 20 ms en respuesta a ello. De lo contrario, el ojo humano no lo verá.
Una versión más sofisticada del método del LED es usar un osciloscopio. Por eso sugerí comprar un osciloscopio y familiarizarse con él en la primera página de esta serie. La idea es conectar varias señales del interior de la FPGA a sus pines de salida accesibles con una sonda de osciloscopio. En proyectos reales, no es raro tener un conector dedicado en la PCB, donde el diseñador de la placa ha reunido un montón de pines de la FPGA sin usar, para que puedan utilizarse para depurar.
Comparado con el analizador lógico en chip, el osciloscopio puede parecer mediocre: puede monitorizar dos señales a la vez, quizá algunas más con osciloscopios más caros. Su ancho de banda es limitado, así que las señales realmente rápidas pueden pasarse por alto. Y aun así, a veces es la herramienta más potente que tenemos a mano. En particular cuando hay patrones de señal repetidos, observarlos con un osciloscopio puede ayudar a pillar qué va mal.
Y a veces se necesita el osciloscopio para entender qué ocurre al nivel del hardware puro y duro. ¿Son correctas las tensiones? ¿Tienen las señales digitales el aspecto que deberían? ¿Hay algún ruido extraño en la señal de tensión?
Y esto me lleva al tipo de depuración más mundano. Más a menudo de lo que queremos creer, el problema es un fallo de hardware realmente tonto, en particular con PCBs desarrolladas para un proyecto concreto. Puede que una de las tensiones de alimentación sea inestable, quizá con picos o caídas breves ocasionales. Puede que el oscilador de reloj no se comporte como se espera, generando una señal de reloj inadecuada. Siempre es buena idea mirar estas cosas antes de desarrollar teorías sofisticadas.
Así que, si quieres llegar a ser bueno en el diseño de FPGA, necesitas un poco de ambas cosas: métodos estériles para extraer datos del interior de la FPGA y también mancharte las manos con el hardware.
Y no lo olvides nunca: solo hay una herramienta de depuración que es realmente eficiente, si se usa correctamente: tu propio cerebro.
Lenguajes de programación
Aunque la programación no está directamente relacionada con el diseño de FPGA, se necesita un conjunto mínimo de habilidades de programación para hacer el trabajo. Por ejemplo, he mencionado antes que los datos de prueba para la simulación a menudo se crean con un programa o script escrito específicamente para ello.
Mencionaré algunos lenguajes de programación que puede que merezca la pena dominar. De todas las cosas que sugiero aprender, esta es en realidad la parte más fácil, y desde luego no hay necesidad de saber todo esto desde el primer día. Ni nunca, ya puestos.
- Tcl: este lenguaje se usa habitualmente para escribir scripts que ejecuta Vivado y otras herramientas de desarrollo de FPGA. Los comandos de una sola línea también son útiles a menudo. Sin embargo, aprender la sintaxis completa de este lenguaje y todas sus características no tiene sentido. Probablemente nunca hagas programación de verdad en este lenguaje, pero sí se recomienda dominar lo básico de su sintaxis. En particular, aprende los distintos tipos de comillas y corchetes, y qué significan.
- C: este lenguaje está tan extendido que consideraría que no saberlo es una especie de handicap. Para el desarrollo de FPGA, a menudo es un buen candidato para crear grandes cantidades de datos de prueba.
- Python: es un lenguaje de scripting popular, que puede ser útil para generar datos de prueba, pero aún más importante: para generar Verilog que implique código repetitivo que no se expresa bien con las propias capacidades sintácticas de Verilog. A menudo es más fácil escribir un script que escriba código Verilog basado en plantillas de código que hacerlo directamente con Verilog.
- Perl: este lenguaje se puede usar para los mismos propósitos que he mencionado antes para Python. En mi opinión, Perl es sin duda mejor para hacer prácticamente todo, pero Python es más popular porque lo adoptaron las universidades: la sintaxis de Python es la que les gusta a los académicos de los lenguajes de programación, y Perl tiene una sintaxis muy elegante y pragmática, pero rompe las reglas académicas sobre cómo debería ser un lenguaje de programación. Así que Perl es estupendo para hacer el trabajo, pero se le considera un lenguaje anticuado, porque la mayoría de la gente considera a Python el lenguaje de scripting principal. Si no estás ya metido en Python, plantéate Perl en su lugar. No te arrepentirás.
- Makefile y scripting de shell de Bash: si ya estás familiarizado con ellos, tenlos en cuenta para construir algunos proyectos. Si no, no los pondría como alta prioridad. Es muy difícil escribir un Makefile adecuado para un proyecto de FPGA, porque las dependencias no siempre son tan directas como en una compilación de software corriente. Y aun así, yo uso Makefiles cuando el proceso de compilación es complejo y quiero construir solo las partes que requieren recompilación.
Hay otros cuantos lenguajes, por ejemplo MATLAB, que también pueden ser útiles, en particular para crear datos de prueba para simulaciones.
Otro aspecto de conocer lenguajes de programación es que a menudo crean un lenguaje común con el equipo de software. Esto es relevante para proyectos de FPGA que implican algún tipo de procesador embebido o incluso un ordenador. Así que ser tú mismo programador a cierto nivel ayuda a comunicarte con los chicos del software. Además, a menudo es más fácil escribir el programa de bajo nivel que se comunica con tu diseño de FPGA que conseguir que lo haga otra persona a partir de una especificación.
Y como recomendación general, aprende a usar Git y úsalo, incluso si trabajas solo en un proyecto. El control de versiones no solo sirve para contentar a los jefes, sino que es una herramienta valiosa que te permite probar algo, luego volver atrás y luego arrepentirte de haber vuelto atrás. Y después de que hayas terminado de trastear, nada de ese desastre queda a la vista.
Yo uso gitk para obtener una representación gráfica del árbol de versiones.
Aquí termina la tercera página de esta serie. La página siguiente trata también de habilidades prácticas, pero de las menos importantes para empezar. El punto principal es explicar cómo y cuándo pueden ser importantes.