A la hora de verificar dispositivos de mediana o alta complejidad uno de los aspectos más poderosos del lenguaje VHDL es el manejo de archivos. Esto nos permitirá tomar estímulos almacenados en un archivo y/o almacenar los resultados obtenidos.
Es posible generar estímulos altamente complejos utilizando herramientas externas a nuestro banco de pruebas. Dichas herramientas pueden ser escritas en lenguajes tales como C o MATLAB.
También suele resultar muy útil realizar un análisis de los resultados utilizando herramientas de alto nivel. Un ejemplo interesante es la verificación de un circuito generador de video utilizando una herramienta externa capaz de graficar la imagen que obtendremos al conectar nuestro dispositivo a un monitor.
En estas, y otras ocasiones, es muy poderoso poder manejar archivos de datos. En las siguientes secciones se introducen los conceptos básicos para el manejo de archivos en VHDL.
Algunas herramientas de síntesis soportan el manejo de archivos, siempre y cuando este se resuelva por completo para t=0. Pero esto no es soportado por todas las herramientas y por lo tanto no es recomendable. |
El lenguaje VHDL posee un manejo de archivos un tanto particular y que no coincide con lo que soportan otros lenguajes. En particular es mucho más limitado que en lenguaje C. Por lo tanto conviene aclarar algunos detalles, que de otra manera llamarían a confusión.
El manejo estándar de archivos en VHDL está orientado a archivos de texto, no es estándar el manejo de archivos binarios.
La lectura y escritura de datos a un archivo se encuentra orientada a líneas, no a caracteres sueltos. Por lo que veremos que el mecanismo básico de escritura consiste en armar una línea de texto y luego escribirla al archivo. Análogamente, la lectura básica consiste en leer una línea completa y luego extraer la información que contiene por partes.
Si bien el manejo de archivos es parte del VHDL, los tipos de datos y funciones involucradas se encuentran declarados en un paquete que no es automáticamente incluido. La biblioteca necesaria se denomina std y el paquete en cuestión es el textio. En el Ejemplo 2-11 vemos el código necesario para incluir estas declaraciones.
En los lenguajes de programación se conoce con este nombre a los identificadores asociados con los archivos que estamos manejando. Esto nos permite operar con distintos archivos en forma simultánea, cada uno con su propio identificador.
En la mayor parte de los lenguajes es posible almacenar este identificador en una variable. En VHDL se utiliza un tipo de identificadores especiales. No se trata de un tipo de datos especial, sino de un elemento del lenguaje completamente diferente. Este elemento es denominado file. Otro detalle interesante es que podemos asociar este identificador con un archivo para t=0, es decir que desde el comienzo de la simulación.
Al igual que otros elementos pertenecientes a la implementación de una entidad, los file se declaran en la arquitectura, antes del begin. La declaración de un file se escribe así:
Como se mencionó en Sección 2.8.3, es posible asociar un identificador con un archivo en disco para t=0. En este caso se realiza la apertura de dicho archivo antes de comenzar la simulación. Para esto basta con usar la siguiente sintaxis en la declaración misma:
El MODO
puede ser read_mode,
write_mode o append_mode. Es decir:
lectura, escritura y agregar al final.
Este mecanismo puede resultar un tanto confuso y se aleja bastante de lo que se usa en lenguajes de programación. Por esta razón existen funciones de apertura (file_open) y cierre (file_close) de archivos.
Para mostrar el uso de estas funciones lo haremos a través del
Ejemplo 2-12. En este caso hemos elegido f
como identificador (file handler). El proceso
etiquetado archivo
es un proceso secuencial que no posee
lista de sensibilidad, muy frecuente en los bancos de pruebas. Dentro de dicho
proceso podemos ver como se utiliza file_open. El primer
argumento es de tipo file_open_status, en este caso la
variable status
. Esta variable es pasada por referencia, es
decir que será modificada por file_open, en la misma
se guardará el resultado de la operación. Los valores que puede tomar
status
son: open_ok (se pudo abrir),
status_error (el identificador ya estaba asociado con un
archivo), name_error (el archivo no existe y/o no pudo
crearse, dependiendo del modo) y mode_error (no se pudo
abrir en el modo requerido). Los otros argumentos son el identificador, el
nombre del archivo y el modo de trabajo. En el ejemplo se muestra como podemos
hacer para abortar la ejecución del banco de pruebas en el caso en que no
se haya podido abrir correctamente el archivo. Para cerrarlo
basta con usar file_close indicando el identificador.
Notar que este proceso termina con un wait y por lo tanto
será ejecutado sólo una vez, caso contrario se volvería a abrir el archivo y
demás.
Ejemplo 2-12. Ejemplo de como abrir y cerrar un archivo
library std; use std.textio.all; entity Ejemplo is end entity Ejemplo; architecture Files of Ejemplo is file f : text; begin archivo: process variable status : file_open_status; begin file_open(status,f,"Nombre",read_mode); assert status=open_ok report "No se pudo abrir" severity failure; -- Acá leemos el contenido del archivo file_close(f); wait; end process archivo; end architecture Files; -- Entity: Ejemplo
Como ya se explicó en Sección 2.8.1, las operaciones de lectura y escritura se encuentran orientadas a líneas completas de texto. En esta sección veremos el tipo de datos involucrado y las funciones que nos permiten leer y escribir líneas completas.
Debido a que el largo de las líneas no es conocido carecería de sentido representarlas con el tipo string, lo que necesitamos es algo así como el equivalente a un puntero a un buffer. Este elemento se encuentra definido como line (access string). Las operaciones de lectura y escritura toman como argumento un objeto del tipo line. El mismo es capaz del albergar el texto de una linea completa, sin importar cuan larga sea.
Para leer un archivo utilizaremos la función readline,
la misma leerá una línea completa del archivo de entrada. Para saber cuando hemos
leído todas las líneas se utiliza la función endfile, que
devuelve true cuando llegamos al final del mismo. En el
Ejemplo 2-13 vemos una arquitectura que abre un archivo
y lee todas sus líneas. Las mismas van siendo leídas una a una en la variable
l
.
Ejemplo 2-13. Ejemplo de como abrir y cerrar un archivo
architecture Lectura of Ejemplo is file f : text open read_mode is "Nombre"; begin archivo: process variable l : line; begin while not(endfile(f)) loop readline(f,l); -- Acá deberíamos extraer lo que queremos de la línea end loop; wait; end process archivo; end architecture Lectura; -- Entity: Ejemplo
En el caso de las escrituras se utiliza la función writeline, cuyo uso es análogo a readline, esto es: writeline(IDENTIFICADOR,LINEA).
Una vez que hemos leído el contenido de una línea procederemos a extraer la información contenida en dicha línea. La idea es que iremos retirando el primer dato que contiene la línea, luego el segundo y así hasta agotar el contenido de la misma.
Para esto el paquete textio nos proporciona un juego de funciones llamadas read. Estas funciones toman dos formas generales: read(LINEA,ELEMENTO) y read(LINEA,ELEMENTO,EXITO). El ELEMENTO será una variable o señal capaz de representar los datos que queremos extraer, ahí se guardará lo que retiramos de la línea. La primer forma de estas funciones abortará la ejecución si no es posible extraer el ELEMENTO indicado. En la segunda forma se indica en EXITO, que es un boolean, si fue posible (true) o no leer el ELEMENTO requerido. El paquete declara un total de ocho variantes, cada una con las dos formas, totalizando 16 funciones de lectura. Las mismas cubren los tipos de datos básicos boolean, character, integer, real, string y time (además de bit y bit_vector, que no son de gran utilidad).
Es importante tener en cuenta que lo que buscará la función read es la representación en texto del tipo de datos indicados. Por ejemplo, en el caso de leerse un boolean se espera encontrar el texto TRUE o FALSE. Otro detalle es que la lectura de los tipos boolean, integer, real y time toleran que hayan espacios antes del valor a leer, esto lo veremos en un ejemplo. Finalmente cabe aclarar que el ELEMENTO a leer debe ser guardado en una variable, si lo que necesitamos asignar es una señal deberemos leer primero el valor en una variable y luego transferirlo a la señal.
Se trata del proceso inverso al de la lectura. En este caso partimos de una linea vacía y vamos agregando datos a la misma. Una vez que hemos concluido con esa línea la escribimos usando writeline. En este momento la línea vuelve a estar vacía y podemos armar una nueva línea.
Para agregar datos a una línea textio provee ocho funciones que cubren los tipos básicos. Seis de ellas son iguales y dos proveen la posibilidad de ajustar parámetros específicos relacionados con los tipos de datos que manejan.
El caso más común cubre los tipos de datos boolean, character, integer y string (además de bit y bit_vector). En este caso el formato es:
Los argumentos JUSTIFICADO y ANCHO pueden omitirse, quedando un formato análogo al read. El JUSTIFICADO indica si el texto estará a la derecha (right) o a la izquierda (left) del espacio usado por la representación en texto del ELEMENTO, si se omite se selecciona justificado a derecha. El ANCHO se utiliza para reservar un ancho fijo para el texto, se rellenan con espacios los lugares no usados. Esto es útil para tipos de datos de longitud variable, como integer, cuando queremos mantener el texto encolumnado. Por defecto se infiere 0, esto quiere decir que el texto ocupa lo necesario.
Para el caso de los ELEMENTOs tipo real se agrega un argumento extra que nos permite seleccionar cuantos decimales imprimir. En caso de omitirse se asume 0, esto quiere decir que se usará notación científica.
Para el caso de los ELEMENTOs tipo time se agrega un argumento extra que nos permite seleccionar en que unidades de tiempo se representará el ELEMENTO.
Al igual que en lenguaje C, es posible acceder a la entrada y salida estándares
de nuestro programa utilizando file handlers predefinidos.
En el caso de VHDL se denomina input
a la entrada estándar y
output
a la salida estándar. Estas dos variables son de tipo
file y están disponibles para su uso. Son los equivalentes a
stdin
y stdout
del lenguaje C.
Con esto estamos en condiciones de escribir el tradicional programa "Hola mundo" que suele mostrar como funciona un lenguaje de programación, su versión VHDL se ve en el Ejemplo 2-14.
Ejemplo 2-14. Ejemplo "Hola mundo" en VHDL
library std; use std.textio.all; entity Hola is end entity Hola; architecture Ejemplo of Hola is begin hola_mundo: process variable l : line; begin write(l,string'("¡Hola mundo!")); writeline(output,l); wait; end process hola_mundo; end architecture Ejemplo; -- Entity: Hola
En el ejemplo notamos que la cadena de caracteres aparece de una manera un tanto particular: string'("¡Hola mundo!"), esto es necesario debido a que la función write se encuentra definida tanto para el tipo string como para el tipo bit_vector. Sin esto la herramienta no puede determinar a cual de los dos tipos de datos nos estamos refiriendo, ya que ambos pueden representarse como cadenas de caracteres.
Esto no debe confundirse con la sentencia report o
assert, aquí estamos escribiendo directamente en la salida estándar.
Un detalle importante es que las sentencias mencionadas escriben a la salida de
errores estándar (stderr
).
Un uso muy poderoso de este concepto es la posibilidad de utilizar pipes y/o redirección de archivos. Esto nos permite conectar la salida de un generador de estímulos a la entrada de nuestro banco de pruebas y/o conectar la salida de nuestro banco de pruebas a un programa que analice los resultados. En sistemas operativos POSIX (como UNIX y Linux) se puede escribir:
A modo de ejemplo, y a los fines de consolidar los conceptos explicados, veremos un ejemplo de como escribir un conjunto de datos a un archivo. A continuación veremos como leer esos mismos datos.
Para la escritura, y por simplicidad, definiremos unas señales que son vectores conteniendo los datos a escribir. El archivo resultante contendrá en cada renglón: un valor tipo boolean, un tiempo expresado en nanosegundos y un entero. Los valores se separarán con comas y se tratará de mantener los mismos encolumnados para facilitar su lectura. El Ejemplo 2-15 muestra la implementación propuesta. El resultado es escrito en el archivo test.txt que se muestra en Ejemplo 2-16
Ejemplo 2-15. Escritura de datos en VHDL
library IEEE; use IEEE.std_logic_1164.all; use IEEE.numeric_std.all; library std; use std.textio.all; entity Escritura is end entity Escritura; architecture Ejemplo of Escritura is -- Un separador entre los valores, es un simple detalle constant SEPARADOR : string(1 to 2):=", "; -- Nuestro identificador file f : text; -- Tipos de datos para los vectores a usar type vect_bool is array(natural range <>) of boolean; type vect_int is array(natural range <>) of integer; type vect_time is array(natural range <>) of time; -- Vectores con los datos a escribir, podrían ser constantes, -- pero asumimos que no lo son. signal buleanos : vect_bool(1 to 5):=(true,false,true,false,false); signal enteros : vect_int(1 to 5):=(7,4,5,23,12); signal tiempos : vect_time(1 to 5):=(1 ns,3 ps,12 ms,10000 fs,100 ns); begin escribir: process variable l : line; variable status : file_open_status; begin -- Creamos el archivo file_open(status,f,"test.txt",write_mode); assert status=open_ok report "No se pudo crear test.txt" severity failure; -- Barremos los elementos de los vectores for i in 1 to 5 loop -- buleanos'range -- Armamos una línea write(l,buleanos(i),right,5); write(l,SEPARADOR); write(l,tiempos(i),right,12,ns); write(l,SEPARADOR); write(l,enteros(i)); -- La escribimos writeline(f,l); end loop; -- Cerramos el archivo file_close(f); -- Fin del proceso secuencial wait; end process escribir; end architecture Ejemplo; -- Entity: Escritura
Ejemplo 2-16. Contenido del archivo test.txt obtenido
TRUE, 1 ns, 7 FALSE, 0.003 ns, 4 TRUE, 12000000 ns, 5 FALSE, 0.01 ns, 23 FALSE, 100 ns, 12
Para la lectura se propone leer el archivo antes generado y contabilizar la cantidad de valores en true de la primer columna, sumar los tiempos de la segunda columna y sumar los valores de la tercer columna. El Ejemplo 2-17 muestra la implementación propuesta. El resultado obtenido se muestra en Ejemplo 2-18. Notar que la suma de los tiempos contiene un error de redondeo, cometido por la herramienta usada debido a limitaciones de la representación interna utilizada para las magnitudes de tiempo.
Ejemplo 2-17. Lectura de datos en VHDL
library IEEE; use IEEE.std_logic_1164.all; use IEEE.numeric_std.all; library std; use std.textio.all; entity Lectura is end entity Lectura; architecture Ejemplo of Lectura is -- Nuestro identificador file f : text; begin leer: process variable l : line; variable status : file_open_status; -- Acumuladores variable a_bool : integer:=0; -- Booleanos variable a_tiempo : time:=0 fs; -- Tiempo variable a_int : integer:=0; -- Enteros -- Variables auxiliares variable bool : boolean; variable tiempo : time; variable int : integer; variable separa : string(1 to 2); begin -- Abrimos el archivo file_open(status,f,"test.txt",read_mode); assert status=open_ok report "No se pudo abrir test.txt" severity failure; while not(endfile(f)) loop readline(f,l); -- Acá extraemos lo que queremos de la línea read(l,bool); if bool then a_bool:=a_bool+1; end if; read(l,separa); read(l,tiempo); a_tiempo:=a_tiempo+tiempo; read(l,separa); read(l,int); a_int:=a_int+int; end loop; -- Cerramos el archivo file_close(f); -- Informamos los resultados write(l,"Booleanos en TRUE: "&integer'image(a_bool)); writeline(output,l); write(l,"Suma de tiempo: "&time'image(a_tiempo)); writeline(output,l); write(l,"Suma de enteros: "&integer'image(a_int)); writeline(output,l); -- Fin del proceso secuencial wait; end process leer; end architecture Ejemplo; -- Entity: Lectura
La escritura de los tipos de datos pertenecientes a la norma IEEE 1164 (std_logic, std_logic_vector, etc.) no se encuentra definida. Por esta razón no es posible escribir o leer este tipo de datos sin la ayuda de funciones auxiliares. Cabe aclarar que la empresa Synopsys define un paquete denominado std_logic_textio dentro de la biblioteca IEEE, este paquete no es parte de ninguna norma IEEE y se trata de una extensión creada por dicha empresa.
La mayor parte de las herramientas de simulación incluyen las extensiones de Synopsys dentro de la biblioteca IEEE estándar. Por lo que una opción consiste en utilizar dichas extensiones. El problema es que la portabilidad de este mecanismo no está asegurada, por otro lado es una violación de los estándares.
Otra opción consiste en utilizar las extensiones de Synopsys incluyendo su código fuente. Estas extensiones son de libre distribución.
Finalmente podemos optar por utilizar nuestras propias funciones. En el Ejemplo 2-19 se muestra una posible implementación realizada por Salvador E. Tropea y Rodrigo A. Melo, la misma se encuentra bajo licencia GPL.
Ejemplo 2-19. Lectura y escritura de datos IEEE 1164
procedure write(l: inout line; v: in std_logic_vector) is variable i : integer; variable s : string(3 downto 1); begin for i in v'range loop s:=std_logic'image(v(i)); write(l,s(2)); end loop; end procedure write; procedure write(l: inout line; v: in std_logic) is variable s : string(3 downto 1); begin s:=std_logic'image(v); write(l,s(2)); end procedure write; procedure read(l: inout line; v: out std_logic_vector) is variable s : string(v'length downto 1); begin read(l,s); str2sv(s,v); end procedure read; procedure read(l: inout line; v: out std_logic_vector; good: out boolean) is variable s : string(v'length downto 1); variable gd : boolean; begin read(l,s,gd); good:=gd; if gd then str2sv(s,v); end if; end procedure read; procedure read(l: inout line; r: out std_logic) is variable s : string(1 to 1); variable v : std_logic_vector(0 downto 0); begin read(l,s); str2sv(s,v); r:=v(0); end procedure read; procedure read(l: inout line; r: out std_logic; good: out boolean) is variable s : string(1 to 1); variable v : std_logic_vector(0 downto 0); variable gd : boolean; begin read(l,s,gd); good:=gd; if gd then str2sv(s,v); r:=v(0); end if; end procedure read; procedure str2sv(s: in string; sv: out std_logic_vector) is variable i : integer; begin for i in s'range loop if s(i)='U' then sv(i-1):='U'; elsif s(i)='X' then sv(i-1):='X'; elsif s(i)='0' then sv(i-1):='0'; elsif s(i)='1' then sv(i-1):='1'; elsif s(i)='Z' then sv(i-1):='Z'; elsif s(i)='W' then sv(i-1):='W'; elsif s(i)='L' then sv(i-1):='L'; elsif s(i)='H' then sv(i-1):='H'; elsif s(i)='-' then sv(i-1):='-'; else report "Elemento desconocido" severity failure; end if; end loop; end procedure str2sv;