Un error frecuente entre los novatos en diseño con FPGA y VHDL es tratar de realizar operaciones con decimales directamente en el lenguaje. Esto es un error porque, aunque existen librerías que soportan esas operaciones, a la hora de convertir el diseño a hardware real, estas operaciones no son sintetizables, es decir, la herramienta de síntesis no sabe como convertir la suma o multiplicación de números con decimales a un bloque electrónico que haga la operación.

Esto genera bastante confusión, ya que requiere un cierto conocimiento de cómo operar con números decimales en aritmética binaria. Este artículo no pretende ser un tutorial exhaustivo de cómo operar en decimal, para eso están los libros de texto. Lo que sí intentaremos es presentar algunos conceptos para que llegado el caso sepamos cómo enfrentarnos al problema.

Si recordamos otros artículos previos, en un sistema digital únicamente trabajamos con 1 y 0 y a través de la aritmética binaria construimos números positivos y negativos. Para construir números decimales debemos utilizar también 1 y 0. En general existen dos formas de representar decimales.

Punto fijo en VHDL

Un número en punto fijo se representa mediante una parte entera y una parte decimal, cada una de ellas con un número fijo de bits, de ahí su nombre.

Por ejemplo si tenemos números de 16 bits, podemos decidir utilizar 10 bits para la parte entera y 6 para la parte decimal.

Para la parte entera podemos decidir que solo utilizamos valores positivos en cuyo caso con este ejemplo tendremos valores entre 0 y 1024, o podemos trabajar con negativos y decidir utilizar una representación en complemento a 2 o con magnitud y signo en cuyo caso podremos representar números entre -512 y 512 si suponemos que el bit mas significativo es el de signo.

Con la parte decimal podremos representar valores con un paso de 2^-6, es decir 0.015625, esta es nuestra precisión. Si queremos aumentarla, deberíamos incrementar el número de bits de la parte decimal.

La gran ventaja de utilizar representación en punto fijo es que podemos usar los bloques típicos de suma, resta y multiplicación de enteros para operar con ellos y ¡funcionan!

Fijaos en estos ejemplos. El primero es una suma en punto fijo. Como se ve, simplemente sumamos los valores sin hacer caso del punto.

  0110.1010        6.6250
+ 0011.0001      + 3.0625
= 1001.1011      = 9.6875

En el caso de la resta es igual, tomamos el valor a restar, calculamos su complemento a dos y sumamos, como con números enteros.

  0011.1010        3.6250
+ 1110.1000      - 1.5000
= 0010.0010      = 2.1250

Y para multiplicar es igual también, simplemente tened en cuenta que el resultado tiene el doble de bits, tanto en la parte entera como en la decimal, exactamente igual que en una multiplicación de enteros.

En este caso no vamos a poner código VHDL ya que no hay ninguna diferencia entre trabajar con punto fijo y representaciones de enteros. Aun así si queréis módulos para trabajar en punto fijo en Opencores tenéis una colección de ellos. En concreto esta es bastante buena,

La ventaja obviamente es la simplicidad. La desventaja, que el rango es muy limitado, lo cual es valido para ciertas aplicaciones, como aquellas en las que leemos valores de un ADC, pero para cálculo científico no es suficiente. En ese caso tenemos que utilizar representaciones de coma flotante, lo cual es bastante más complejo.

Coma flotante en VHDL

Los números en coma flotante nos permiten representar valores en un rango muchísimo más grande. Dentro de los computadores se utiliza el estándar IEEE 754 para representar números en coma flotante.

Un numero en com flotante es de la forma:

Donde 4654656 es la mantisa y -8 el exponente. De esta forma el estándar almacena 3 valores, el signo, la mantisa y el exponente. Si utilizamos precisión simple utilizará 32 bits para almacenarlo y si utilizamos precisión doble utilizará 64 bits.

En la representación de 32 bits, se utiliza la siguiente estructura.

Por lo tanto para almacenar el valor que hemos indicado anteriormente tendremos que convertir 4654656 a binario con 23 bits y el -8 al formato del exponente que se calcula sumando 127 al valor, para que siempre sea un valor positivo representado con 8 bits, en este caso 127-8=119. Por lo que el número nos queda:

00111011110001110000011001000000

Ahora que ya sabemos como se representa en este formato, podemos intentar sumar dos números en formato IEEE 754, pero veremos que no es algo evidente. Por ejemplo si queremos sumar:

El proceso de sumarlos es complejo. No pretendo ahora hacer un tratado completo de aritmética en coma flotante, tenéis montones de tutoriales en Internet. Pero para entender la complejidad, el proceso consiste en poner los dos números con el mismo exponente para poder sumar las mantisas. Después hay que normalizar el exponente resultante para que el resultado tenga de nuevo cero como parte entera. Es por tanto un proceso complejo que requiere hardware especializado para realizarse y varios ciclos de reloj. Los módulos hardware especializados en realizar estas operaciones son las famosas Unidades de Coma Flotante.

Estas unidades realizan sumas, restas, multiplicaciones y divisiones en coma flotante mediante los algoritmos correspondientes.

Por suerte los fabricantes de FPGA incluyen entre las librerías de componentes bloques para realizar operaciones de coma flotante. En concreto en Xilinx Vivado tenemos un asistente dentro del IP Catalog que nos permite generar el modulo aritmético que necesitemos en nuestros diseños y configurar su latencia y rendimiento en ciclos por operación. Es decir, es un bloque segmentado.