2.8. Manejo de archivos

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.

Atención

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.

2.8.1. Modelo de archivos usado en VHDL

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.

2.8.2. Biblioteca y paquete necesarios

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.

Ejemplo 2-11. Biblioteca y paquete necesarios para el manejo de archivos

library std;
use std.textio.all;

2.8.3. File handlers

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í:

file IDENTIFICADOR : text;

2.8.4. Abrir y cerrar archivos

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:

file IDENTIFICADOR : text open MODO is "Nombre_archivo";

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

2.8.5. Lectura y escritura de líneas completas

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).

2.8.6. Leyendo el contenido de una línea

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.

2.8.7. Armando el contenido de una línea

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:

write(LINEA,ELEMENTO,JUSTIFICADO,ANCHO)

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.

2.8.8. Entrada y salida estándar

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:

$ programa_generador | testbench | programa_procesador

2.8.9. Ejemplo de escritura y lectura

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

Ejemplo 2-18. Resultados mostrados por el ejemplo de lectura

Booleanos en TRUE: 2
Suma de tiempo: 12000101400000 fs
Suma de enteros: 51

2.8.10. Tipos IEEE

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;
Copyright © 2011 UTN FRBA - INTI - Ing. Salvador E. Tropea