Clash

De Wikihaskell
Saltar a: navegación, buscar
CλaSH
Lenguaje de descripción de hardware basado en Haskell
Lenguaje Haskell
Biblioteca CλaSH
Autores Alejandro Calderón Sánchez
Carlos Rodríguez Rosano
Alfonso Sendra Gamero

CλaSH es un lenguaje de descripción de hardware que toma prestado tanto su sintaxis como su semántica del lenguaje de programación funcional Haskell. Polimorfismo y funciones de orden superior proporcionan un nivel de abstracción y generalidad que permiten a los diseñadores de circuitos describir los circuitos de una manera más natural que con los lenguajes de descripción de hardware tradicionales.

Las descripciones de circuitos pueden ser traducidas a VHDL utilizando el propio compilador de Haskell. Como las descripciones de circuitos, el código a simular y las entradas de prueba también son implementables en Haskell, simulaciones completas pueden ser realizadas por el compilador de Haskell.


Contenido

Instalación

Se deberá tener instalada una versión de la plataforma Haskell que contenga al menos la versión 6.12 del compilador GHC.

Lo primero es instalar las dependencias:

$ sudo apt-get install libedit2 libedit-dev freeglut3-dev libglu1-mesa-dev libgmp3-dev

Descargamos los fuentes de GHC que van a ser necesarios para compilar la plataforma Haskell:

$ wget http://www.haskell.org/ghc/dist/6.12.3/ghc-6.12.3-i386-unknown-linux-n.tar.bz2

Nota: Mirar arquitectura para descargar el archivo adecuado(32/64 bits).

Descomprimimos el archivo que nos hemos descargado:

$ tar -xvvf ghc-6.12.3-i386-unknown-linux-n.tar.bz2

Compilamos los fuentes:

$ cd ghc-6.12.3/
$ ./configure
$ sudo make install

Descargamos los fuentes de la plataforma Haskell:

$ wget http://hackage.haskell.org/platform/2010.2.0.0/haskell-platform-2010.2.0.0.tar.gz

Descomprimimos el archivo que nos hemos descargado:

$ tar -xvzf haskell-platform-2010.2.0.0.tar.gz

Compilamos los fuentes:

$ cd haskell-platform-2010.2.0.0/
$ ./configure
$ make
$ sudo make install

Procederemos a descargar la última versión de la librería CλaSH de su página de descargas.

$ wget http://clash.ewi.utwente.nl/ClaSH/Downloads_files/clash-bin-0.1.1.tar.gz

Desempaquetamos el fichero tar.gz descargado y lo instalamos ejecutando el siguiente comando en el directorio que se ha creado:

$ tar -xvzf clash-bin-0.1.1.tar.gz
$ cd clash-bin-0.1.1/
$ cabal update
$ cabal install

Una vez que cabal ha finalizado la instalación podemos ejecutar el intérprete de CλaSH desde la línea de comandos mediante:

$ $HOME/.cabal/bin/clash --interactive

Este intérprete presenta la misma funcionalidad y comandos que el de GHC.


Creando el primer circuito

Vamos a definir un semisumador.

Halfadder.PNG

Un semisumador es un componente que se usa en la implementación de un sumador binario. Toma dos bits como entrada y los suma. Los resultados son un bit de suma y un bit de acarreo. Habitualmente se implementa mediante una puerta lógica XOR y otra AND.

Aquí está como definimos el semisumador:

 import CLasH.HardwareTypes
 halfAdd a b = (sum, carry)
   where
     sum   = hwxor a b
     carry = hwand a b

Importamos un módulo llamado CLaSH.HardwareTypes, que define ciertas operaciones que podemos utilizar para construir circuitos. Concretamente contiene las puertas lógicas XOR (hwxor) y AND (hwand).

Aquí vemos como simulamos el semisumador con sus dos entradas con valor de uno lógico (High):

 *Main> halfAdd High High
 (Low,High)


Creando el segundo circuito

Vamos a definir un sumador binario completo.

Fulladder.PNG

Como se puede ver está compuesto de dos semisumadores. Aquí está como lo definimos (en el mismo módulo donde definimos el semisumador):

 fullAdd carryIn (a, b) = (sum, carryOut)
   where
     (sum1, carry1) = halfAdd a b
     (sum, carry2)  = halfAdd carryIn sum1
     carryOut       = hwxor carry2 carry1

Observemos como la segunda entrada son un par de bits que representamos como una tupla. Transcribimos el diagrama del circuito de la figura dándole nombre a todas las señales internas (sum1, carry1 y carry2) y definiendo todos los componentes del circuito.

Podemos simular el circuito llamando a la función, como hicimos en el semisumador, pero como las entradas se van haciendo cada vez más complejas podemos simplificarlo creando casos de prueba:

 test1 = halfAdd Low Low
 test2 = fullAdd Low (High,Low)
 test3 = fullAdd High (Low,High)

Aquí están los resultados de las simulaciones:

 *Main> test3
 (Low,High)
 *Main> test2
 (High,Low)

Generando VHDL

Antes de la generación de VHDL tenemos que añadir alguna información adicional al módulo que contiene la descripción de nuestro circuito. Necesitamos importar un segundo módulo, CLasH.Translator, que contiene una lista de anotaciones que van a ayudar en el proceso de traducir el circuito a VHDL. También tenemos que añadir la anotación TopEntity al sumador completo para indicar que se encuentra en el tope de nuestra jerarquía de circuitos. El módulo para la descripción del sumador completo quedaría de la siguiente forma:

import CLasH.HardwareTypes
import CLasH.Translator
{-# ANN fullAdd TopEntity #-}
fullAdd carryIn (a, b) = (sum, carryOut)
  where
    (sum1, carry1) = halfAdd a b
    (sum, carry2)  = halfAdd carryIn sum1
    carryOut       = hwxor carry2 carry1
halfAdd a b = (sum, carry)
  where
    sum   = hwxor a b
    carry = hwand a b

Desde el intérprete podemos traducir el circuito a VHDL usando el comando :vhdl

 *First> :vhdl
   ...
   Normalization process
   ...
   Output VHDL
   ...
 *First>

Si el compilador no muestra ninguna excepción/error significa que el circuito ha sido traducido exitosamente a VHDL. El código generado lo podemos encontrar el directorio vhdl dentro del directorio de trabajo acutal.

Circuitos complejos

Se va a describir en este punto algunos circuitos más complicados utilizando constructores y tipos de datos abstractos.

Aritméticos

In clash,no solo trabajamos con líneas de bajo nivel como los tipos bits y/o puertas como hwand y hwxor,sino con otros tipos de datos y puertas.Algunos de estos tipos de datos son los enteros(intergers) y existen puertas para trabajar con ellos.También nos encontramos variaciones del tipo enteros(intergers) como pueden ser IntergerT,NaturalT y PositiveT.

Un circuito simple formado por puertas aritmética es el multiplyAccummulate. Este circuito toma tres entradas:x,y y acc.los número x e y se multiplican y el resultado se le suma el acumulador acc.

MultiplyAccumulate.png

mac acc x y = x * y + acc

Podemos simular este circuito de la siguiente forma:

*First> mac 11 12 13
167

La función mac no se puede mandar para que represente su esquema en VHDL , para ello hay que adaptarla.El motivo es que la estructura VHDL solo permite representa funciones con tipos de datos concretos, no funciones polimórficas.

Vamos adaptar la función mac para que tenga unos tipos de datos concretos.

type Int8 = Signed D8
mac8 :: Int8 -> Int8 -> Int8 -> Int8
mac8 acc x y = mac acc x y

Usamos la palabra clave type para crear un sinónimo de tipo,Int8 que será de tipo entero con signo de 8 bits (Signed D8). La función mac8 tiene ahora tres entradas del tipo Int8, y como resultado da un valor de tipo Int8.

Mientras que la función mac original trabaja con números de precisión infinita,la mac8 trabaja con enteros con signo,con una precisión de 8 bits, esto nos puede lleva que en su representación nos lleve a un desbordamiento.Para que en los enteros con signo o sin signo en clash, no produzca un desbordamiento se pasa de un extremo al otro.

Un ejemplo de lo anterior: sumamos 7 al valor 127,y el resultado es -122.

*First> let k = (127::Int8)
*First> k
127
*first> k + 7
-122

Con los cambios que le hemos realizado al circuito mac8 , el compilador de clash generará el VHDL sin ningún error.

-- Automatically generated VHDL
use work.types.all;
use work.all;
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
use std.textio.all;
entity mac16Component_0 is
  port(accAe52 : in signed_16;
       xzAe72 : in signed_16;
       yzAe92 : in signed_16;
       reszAehzAeh3 : out signed_16;
       clock : in std_logic;
       resetn : in std_logic);
end entity mac16Component_0;
architure structural of mac16Component_0 is
begin
      com_ins_reszAehzAeh3 : entity macComponent_1
                                  port map (paramzAeYzAeY2 => acczAe52,
                                            paramzAf0zAf02 => xzAe72,
                                            paramzAf2zAf22 => yzAe92,
                                            reszAf4zAf42 => reszAehzAeh3,
                                            clock => clock,
                                            resetn => resetn);
end architecture structural;
-- Automatically generated VHDL
use work.types.all;
use work.all;
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
use std.textio.all;
entity macComponent_1 is
  port(paramzAeYzAeY2 : in signed_16;
       paramzAf0zAf02 : in signed_16;
       paramzAf2zAf22 : in signed_16;
       reszAf4zAf42 : out signed_16;
       clock : in std_logic;
       resetn : in std_logic);
end entity macComponent_1;
architure structural of mac16Component_1 is
     signal argzAfkzAfk3 : signed_16;
begin
     reszAf4zAf42 <= argzAfkzAfk3 + paramzAeYzAeY2;
     argzAfkzAfk3 <= resize(paramzAf0zAf02 * paramzAf2zAf22, 16);
end architure structural;
     exp

Este circuito lo podremos ver más adelante dentro del grupo de los circuitos secuenciales, utilizando para la acumulación un elemento de memoria.

Elementos de elección

En clash, la elección puede ser alcanzada por medio de un conjunto de elementos sintácticos,formados por: expresiones case,if-then-else,patrones y guardas.La mayoría están realizados con expresiones case.Todos estos elementos de elección sintácticos pueden ser trasladados a la estructura VHDL. Ahora vamos a ver como se utilizaría los cuatro elementos de elección sintáctico para realizar un circuito multiplexor.Un circuito multiplexor tiene dos entradas x e y, y una señal c.La salida se obtiene de la siguiente forma,la salida es la entrada x,si c tiene un nivel bajo, y la salida es la entrada y, si c es un nivel alto.

Ejemplos:

*First> multiplexer Low (1,2)
1
*First> multiplexer High (1,2)
2

Para empezar, vamos a definir el multiplexor usando el constructor de elección más usual,con la expresión if-then-else:

multiplexerIfThenElse c (x,y) =
 if (c == Low) then
  x
 else

Mientras que el resultado de una expresión if-then-else es asignado a una señal,la clausula else deberíamos definirla: i.e. no esta definido para valores no definidos.

multiplexerCase c (x,y) =
 case c of 
   Low -> x
   High -> y

Las expresión anterior, se podría realizar utilizando guardas,y guardas usando la expresión otherwise:

multiplexerGuards c (x,y) | c == Low = x
                          | c == High = y

multiplexerGuards' c (x,y) | c == Low = x
                           | otherwise = y

También es frecuente usar la comparación de patrones para forma expresiones de elección.

multiplexerPatterns Low (x,y) = x
multiplexerPatterns High (x,y) = y

Vamos a ver la función anterior, pero utilizando el patrón de subrayado:

multiplexerPatterns' Low (x,y) = x
multiplexerPatterns' _ (x,y) = y

Ahora vamos a realizar un test de comparación ,en el cual vamos a simular las las salidas en un circuito.Nosotros usaremos todas las funciones definidas en el Prelude,el cual realiza la función test sobre la lista de valores, si todas los valores son iguales a 3,pues la función test4 nos devolverá el valor True.

test4 = all (==3)
  [multiplexerIfThenElse High (2,3)
  ,multiplexerCase       High (2,3)
  ,multiplexerCase'      High (2,3)
  ,multiplexerGuards     High (2,3)
  ,multiplexerguards'    High (2,3)
  ,multiplexerPatterns   High (2,3)
  ,multiplexerPatterns'  High (2,3)
  ]

La ejecución del test4 en el interprete nos devuelve el resultado esperado:

*First> test4
True

Un contador

Vamos a realizar un circuito contador simple utilizando las operaciones aritméticas y elementos de elección antes tratados.

Primero vamos a visualizar el esquema del circuito contador:

Contador.png


El contador contará ascendente o descendente dependiente de la variable direction. La variable direction se realiza,utilizando el tipo de datos enumerado.

data Direction = up | Down | Hold

Primero,vamos a definir la función del circuito usando case y if-then-else.

counter bound direction x = case direction of
 up -> if x < bound then
         x + 1 
       else
         0
 Hold -> x
 Down -> if x > 0 then
           x - 1
         else
           bound

Una segunda versión del contador ,usaremos para ella patrones y guardas.

counter' bound Up   x | x < bound = x +1
                      | otherwise = 0
counter' _     Hold x             = x
counter' bound Down x | x > 0     = x - 1
                      | otherwise = bound


Tipos de Datos

CλaSH proporciona al usuario una gran cantidad de tipos a usar. En los siguientes apartados comentaremos los tipos que actualmente soporta la biblioteca, así como sus detalles más relevantes.

Tipos Básicos

Existen tres tipos básicos en CλaSH, los cuales son todos enumeraciones.

Bit

 data Bit = High | Low

Como su definición indica, el tipo Bit posee dos estados: el de alto nivel (High), equivalente al valor 1; y el de bajo nivel (Low), equivalente al valor 0.

Para trabajar con ellos, la biblioteca proporciona cuatro funciones básicas, correspondientes a las operaciones lógicas and, or, xor y not, las cuales mostramos a continuación:

 hwand :: Bit -> Bit -> Bit          hwxor :: Bit -> Bit -> Bit
 High 'hwand' High = High            High 'hwxor' Low = High
 _ 'hwand' _       = Low             Low 'hwxor' High = High
                                     _ 'hwxor' _      = Low
 hwor :: Bit -> Bit -> Bit
 High 'hwor' _     = High            hwnot :: Bit -> Bit
 _ 'hwor' High     = High            hwnot High       = Low
 Low 'hwor' Low    = Low             hwnot Low        = High

Bool

  data Bool = True | False

Además del tipo booleano, CλaSH da soporte a varios operadores que trabajan con él, tales como:

  • Operadores de comparación: < , > , <= , >= , == y /=
  • Operadores binarios: && y ||
  • Operador unario de negación: not

Enumeraciones

Además de las enumeraciones definidas anteriormente, CλaSH también permite crear nuestros propios tipos de enumeraciones. Estos se construyen usando la misma sintaxis que las enumeraciones anteriores. Por ejemplo, podemos definir el tipo Color como una enumeración de los colores Rojo, Verde y Azul:

data Color = Rojo | Verde | Azul

Tipos Especiales

CλaSH presenta distintos tipos especiales, como el tipo contenedor de estado que informa al compilar de CλaSH si algo forma parte del estado de una función. Otros tipos especiales son los niveles tipo números y booleanos.

Debemos tener en cuenta que estos tipos especiales no tiene representación en tiempo de ejecución, por lo que el usuario nunca debe tratar de evaluar una sentencia que posee alguno de estos tipos.

Niveles de tipo básicos

CλaSH da soporte a diferentes niveles de tipos básicos, que sean básicos quiere decir que se pueden utilizar para los cálculos de nivel de tipo. A pesar de poseer niveles límites de representación, pueden ser usados como argumentos de funciones, siendo su valor actual undefined. El símbolo undefined denota una computación que nunca termina, por lo que el usuario nunca debe evaluar uno de estos términos.

CλaSH soporta niveles de tipo booleanos y numéricos. Permitiendo varios operadores de comparación de booleanos, varias clases numéricas y diferentes operadores aritméticos.

Un importante operador que entra en juego cuando se emplean cálculos de nivel de tipo, es el operador de igualdad de coacción de tipos, ~. El operador ~ pregunta al corrector de tipos si se cumple que el tipo a su izquierda es 'igual' al tipo de su derecha. Ponemos 'igual' entre comillas para indicar que no es que los tipos vayan a ser iguales, sino más bien, que el programa no va a dar error tras la supresión de tipos. A continuación vemos como se utiliza el operador ~, para preguntar al corrector de tipos si el tipo de variable se corresponde con el sucesor del tipo de variable s.

 f :: (Succ s ~ s´) => ...

Respecto a los booleanos, existen dos niveles de tipo, True y False, ya que el sistema de tipos de CλaSH, no permite agrupar ambos tipos en uno sólo. El usuario no debe confundir los tipos True y False con los términos, True y False. Estos términos son del tipo Booleano, pero al mismo tiempo son por ellos mismos tipos.

Hay varios operadores de comparación de nivel de tipo, parecidos a los que conocemos para los booleanos en el nivel de término. No obstan te suelen ser infix y se escriben entre dos puntos (:). Los operadores soportados son:

  • Menor que --> :<:
  • Mayor que --> :>:
  • Mayor o igual que --> :>=:
  • Menor o igual que --> :<=:
  • Igualdad --> :==:
  • And --> :&&:
  • Or --> :||:
  • El operador unario de negación --> Not

Por otro lado, respecto a los numéricos, su nivel de tipo es usado tanto en los tipos vector de tamaño fijo, como en los dos tipos de enteros soportados por CλaSH. El uso de dicho nivel de tipo es algo incómodo, por eso, se han definido sus alias de tipo para los números enteros en el rango de -10000 al +10000. Todos los alias del nivel de tipo entero están prefijadas con la letra mayúscula D. Por ejemplo, el nivel de tipo 2, se escribe como: D2.

Algunas funciones requieren pasar un nivel de tipo entero como argumento, en estos casos, el nivel representación de dicho nivel de tipo entero necesita ser usado. Todos los niveles de representación están prefijados por la letra minúscula d. Así el nivel de representación del nivel de tipo D2, se escribe como: d2.

CλaSH posee tres clases definidas para los niveles de tipo entero (las cuales son sus alias también), las cuales son:

  • IntegerT, que cubre el rango completo de los enteros.
  • NaturalT, que cubre el rango de los enteros naturales.
  • PositiveT, que cubre el rango de los enteros positivos.

Además de las clases numéricas, hay diversos operadores aritméticos también soportados por CλaSH, como el operador de suma :+:, el de resta :-:, el operador sucesor Succ y precesor Pred, ...

Tipos Agregados

Los tipos agregados son tipos que usan un constructor simple para agrupar o agregar a un conjunto de múltiples valores. CλaSH da soporte a unos cuantos tipos agregados, entre los que mencionaremos las tuplas y los registros.

Las tuplas pueden agrupar valores usando la sintaxis ( , ). Para crear duplas, triplas y demás simplemente añadimos más comas a la sintaxis ( , , ). Por lo que todo incremento en el tamaño de la tupla, supone la agregación de otra coma. Como ejemplo simple, podemos usar la tuplas para definir una pantalla de 5 pixels de ancho y 4 pixels de alto, y en donde cada pixel está formado por tres colores. Podemos definir los tipos como siguen:

type Pixel = (Color, Color, Color)
type Row = (Pixel, Pixel, Pixel, Pixel, Pixel)
type Screen = (Row, Row, Row, Row)

A continuación creamos una instancia del tipo pantalla como sigue:

pixel :: Pixel
pixel = (Red, Green, Blue)
row :: Row
row = (pixel, pixel, pixel, pixel, pixel)
screen :: Screen
screen = (row, row, row, row)

El acceso a los valores de una tupla no posee nombre propio, por tanto, si queremos acceder por ejemplo al segundo color de la tupla pixel, tenemos que indicarlo como sigue:

secondColor :: Pixel -> Color
secondColor (_,x,_) = x

Para este pequeño ejemplo la notación es asequible, sin embargo, para una tupla de once elementos o más, la notación comienza a no ser manejable. Por esto y otras razones, CλaSH soporta los registros como tipos agregados, que poseen nombres de acceso.

Redefinamos el Pixel, anteriormente definido como tupla, usando la sintaxis de los registros, Constructor {,}.

data Pixel = Pix {red :: Color, green :: Color, blue :: Color}

Nótese que la sintaxis del registro también incluye un nuevo constructor, Pix, usado para crear un nuevo Pixel como tipo de dato. Redefinamos también la función secondColor usando la sintaxis de los registros.

secondColor :: Pixel -> Color
secondColor Pix {red = red, green = green, blue = blue} = green

La carga de notación ha aumentado en lugar de disminuir, con el uso de los registros. Para omitir la introducción explícita de las funciones de acceso, se incorpora una sintaxis comodín para los registros, Constructor {..}. Esta sintaxis automáticamente introduce los nombres de las funciones de acceso indefinidas dentro del ámbito de la función en la cual se usa:

secondColor :: Pixel -> Color
secondColor Pix {..} = green

La definición de esta última función, comienza a ser fácil y clara de comprender.

Tipos Enteros

Con el fin de formar parte del comportamiento HDL, CλaSH da soporte para los enteros y sus operaciones. Todos los enteros tienen un rango de especificación. Para los enteros de tamaño, el bit de tamaño del entero está codificado en el tipo, esto determina el rango representable. Las matrices de índices seguros codifican un límite superior incluido en su tipo, permitiendo a los enteros un rango desde 0 hasta el límite superior.

Los enteros con la indicación del bit de tamaño puede ser de dos formas: entero con signo o entero sin signo. Los enteros con signo se codifican en complemento a dos, por lo que el rango que pueden representar va desde -2(n-1) a +2(n-1)- 1, donde n es el bit de tamaño del entero. El rango de representación de los enteros sin signo contiene los números desde 0 hasta +2n - 1 .

La declaración de un entero con signo es la siguiente:

newtype (NaturalT size) => Signed size = ...

Por otro lado, la declaración del tipo entero sin signo es:

newtype (NaturalT size) => Unsigned size = ...

Donde la variable size tiene que ser un nivel de tipo entero indicador del bit de tamaño del entero. Por ejemplo, definimos el tipo alias para un entero con signo de 8-bit como sigue:

type Int8 = Signed D8

En general, podemos construir literales enteros de la siguiente forma:

x = (3 :: Int8)

En cuanto a las funciones que podemos realizar con dichos tipos, tenemos que los enteros con signo poseen las siguientes operaciones:

  • Suma: +
  • Resta: -
  • Multiplicación: *
  • Negación: - (operador unario)
  • Desplazamiento de un bit a la izquierda: shiftL
  • Desplazamiento de un bit a la derecha: shiftR

Por otro lado, los enteros sin signo tienen soporte para las mismas operaciones que los enteros con signo, exceptuando el operador unario de negación, que es exclusivo para estos últimos.

Para las operaciones con vectores necesitamos índices seguros, índices con un exclusivo límite superior que dan un error cuando se excede el valor del límite inferior o el del límite superior especificado. Por está razón, CλaSH da soporte al tipo Index:

newtype (NaturalT upper) => Index upper = ...

Donde la variable upper tiene que ser un entero de nivel de tipo que indica el límite superior incluido en el entero. Por ejemplo, definimos el tipo alias para un entero que permite los valores entre 0 y 8 de la siguiente forma:

type WordMax8 = Index D9

En general, lo podemos construir con literales enteros, como sigue:

x = (3 :: WordMax8)

El tipo Index permite las siguientes operaciones:

  • Suma: +
  • Resta: -
  • Multiplicación: *

Referencias

http://clash.ewi.utwente.nl/ClaSH/Index.html

Herramientas personales