Mostrando entradas con la etiqueta ROM. Mostrar todas las entradas
Mostrando entradas con la etiqueta ROM. Mostrar todas las entradas

viernes, 28 de octubre de 2011

Generando el Hex del ASCII para ROM/RAM usando Matlab

En un previo post , http://hdl-fpga.blogspot.com/2011/06/memorias-rom-fpga-vhdl-como.html, detallé la codificación en VHDL para inferir memorias ROM e implementarlas en FPGAs. Una de las partes mas 'aburridas' de esta codificación es primero pasar de la letra al ASCII y después del ASCII al Hex respectivo. Especialmente para el caso de un string o número muy largo, o varios strings. 
Por ello, en uno de mis últimos cursos que dicté uno de mis alumnos no quería 'aburrirse' realizando las dos conversiones, entonces uso sus conocimientos y me presentó una solución tan ingeniosa como efectiva: escribió una pequeña función en Matlab que hace no solo las mencionadas conversiones sino que también genera directamente la sintaxis VHDL respectiva para la asociación del arreglo ROM  con sus valores respectivos. Gracias Ihosvanni por evitar que nos aburramos !. Acá va un screenshot del .m:




Básicamente lo que hace este código .m es tomar tres mensajes, y convertirlos en ASCII y luego al Hex respectivo. Para este caso planteado el resultado de la ejecución es algo como esto: 

0=>x"56",1=>x"61",2=>x"6C",3=>x"6F",4=>x"72",
5=>x"65",6=>x"73",7=>x"20",8=>x"64",9=>x"65",
. . . . . .
51=>x"20",52=>x"21",others=>x"20"


Así, lo único que queda por hacer es copiar y pegar estos valores en el arreglo ROM declarado tal como se detalló en el previo bloq. 
Este .m se puede perfectamente adaptar a distintos casos, por ejemplo remover mensajes, agregar más mensajes, etc. Por supuesto tener la precaución de que si se remueve algún mensaje, se deben remover todas las referencias al mismo, si se agregan mensajes, agregar las referencias respectivas. También se puede usar para casos en los que se quiere inicializar una memoria tipo RAM con un arreglo definido tal como se detalla en el previo blog.

El código Matlab lo puedes bajar de este link: ascii2rom.m

Espero que sea de utilidad, si es así ... bueno, házmelo saber ! 

domingo, 21 de agosto de 2011

Como almacenar en ROM un archivo de audio en Simulink/SysGen

Describiré a continuación los pasos necesarios para poder usar una memoria ROM como medio para almacenar los datos digitalizados de un archivo de audio. Posteriormente, una vez con los datos en la ROM, es posible llevar a cabo el necesario procesamiento de la senal. En caso de implementar el diseño en el FPGA, la inicialización de la memoria ROM es automáticamente llevada a cabo. 

Características del Bloque ROM de SysGen
- Datos son almacenados por individualmente, por palabra, y todos los datos tienen el mismo tipo (signed, unsigned, etc.), el mismo ancho (cantidad de bits) y la misma posición para el punto binario. 
- Cada dato es asociado con exactamente una dirección de memoria.
- El rango de direcciones de la memoria es desde 0 hasta N-1, siendo N la profundidad de la memoria (cantidad de datos). 
- La dirección de la memoria debe ser controlado por un dato entero, sin signo (unsigned) y en tipo punto fijo. 
- El bloque ROM tiene un puerto de entrada para las direcciones, y un puerto de salida para los datos de salida. 
- El bloque ROM puede ser implementado en:
    - RAM distribuida (LUTs y lógica discreta asociada).
    - Bloque dedicados de memoria RAM. 
- La performance máxima de un bloque ROM se logra cuando: 
    - La opción 'Provide reset port for output register' no es seleccionada. 
    - La opcion 'Depth' es menor a 16.384.
    - La 'Latency' es 2 o mayor. 

Como calcular el tamaño del Bloque ROM
Para poder almacenar un archivo de audio en un Bloque ROM es necesario saber el tamano del archivo a fin de poder dimensionar la ROM, pues los parametros principlaes del bloque ROM, tal como se vera mas abajo, son su profundidad (cantidad de datos) y el ancho del datos (cantidad de bits por dato). 
Para obtener esta informacion se hace uso de algunas funciones basicas de Matlab, las cuales son ejecutadas en la ventana de comandos de Matlab:
- 'wavread': ésta función lee un archivo de audio tipo wave, .wav, de Microsoft. 
   Uso 1:
          archivo_audio = wavread('musica.wav);
le asigna a la variable archivo_audio los datos del archivo musica.wav.
   Uso 2:
          tamano = wavread('musica.wav', 'size');
le asigna a la variable tamano la cantidad de datos del archivo 'musica'wav'. 
   Uso 3:
          [archivo_audio_1, Fs, nbits] = wavread('audio.wav');
le asigna a la variable archivo_audio_1 los datos del archivo musica.wav; a la variable Fs la frecuencia de muestreo del archivo musica.wav y a la variable nbits el numero de bits por muestra. 
- 'size': esta función también puede usarse para determinar el tamaño del archivo correspondiente. 
   Uso: 
       tamano_2 = size(wavread('musica.wav'),1);
Notar que el uso de size('  ',1); retorna la cantidad de filas del arreglo '   ', mientras que size('  ',2) retorna la cantidad de columnas del arreglo. 

A continuación se muestra la ejecución de los comandos detallados arriba y su correspondiente resultado en la ventana workspace. 




Configuración de los Parámetros del Bloque ROM
Una vez ejecutados los comandos descritos anteriormente, se puede proceder a la configuración de los parámetros del bloque ROM. Para ello, por supuesto, se debe agregar el bloque ROM al sistema que se quiere diseñar en Simulink. Al hacer doble click sobre el bloque ROM se abrirá la ventana de configuración del bloque. 
Tab 'Basic': 
A fin de escribir los parámetros en forma genérica se puede configurar la ROM usando los comandos antes ejecutados. Estos se detallan en la siguiente figura. 




Tab 'Output Type':
en este tab los parametros se refieren al dato de salida. Por supuesto que existe una relacion directa entre el dato que se grabo en la ROM con el dato que sale de la ROM. Como la grabacion o almacenamiento del dato se hizo en forma automatica, de algun modo se debe conocer el rango de los datos almacenados en la ROM a fin de que los datos de salida sean configurados de modo que se pueda reproducir la senal correctamente. 
Para ello se puede ejecutar en la ventana de comandos de Matlab el comando: 


        rango_datos=wavread('musica.wav')


Para luego inspeccionar la variable rango_datos al presionar dos veces sobre la misma en la ventana Workspace, Así, se mostrará en la ventana 'Variable Editor' del entorno Matlab, todos los datos de la respectiva variable. Por ejemplo, comúnmente se puede observar que los datos .wav tienen un rango desde +1.0 a -1.0. También es posible obtener estos valores usando: 


       max(rango_datos)


       min(rango_datos)

Basado en lo valores obtenidos, +1.0 a -1.0, se puede decir que se necesitan como mínimo 2 bits para la parte entera. Mientras que para la parte decimal, la cantidad de bits dependerá de la precisión que se desea. Aunque una gran precisión implica una gran numero de bits, lo que implica mayores recursos, sobre todo mayor cantidad de bloques ROM. Un valor de compromiso puede ser 6 bits para la parte decimal. La configuracion para este tab puede verse en la siguiente figura. 




Direccionamiento del Bloque ROM 

Normalmente se usa un contador para generar las direcciones del bloque ROM. El contador debe contar desde la dirección 0 hasta la máxima dirección de memoria, que es el valor asignado al parámetro 'Depth' durante la configuración del bloque ROM. 




Esquema del Sistema con Bloque ROM 
Finalmente juntando los bloques, el sistema debería ser semejante al mostrado a continuación.  
Bien, espero que esta guía te haya ahorrado mucho tiempo.... pues yo perdí un monnnnnnnnnnnnnnnnnnntón de tiempo buscando algo similar que nunca pude encontrar.... :) 



miércoles, 8 de junio de 2011

Paquetes: declaración y uso

Paquetes es una herramienta que provee VHDL para facilitar el uso de constantes, funciones, tipos, etc. que pueden ser compartidos entre diferentes componentes.
La sintaxis de la declaración y el cuerpo del paquete se detallan a continuación:

 1 package <package_name> is
 2         [subprograma_declarations];
 3         [constant_declarations];
 4         [type_declarations];
 5         [component_declarations];
 6         [attribute_declarations];
 7         [attribute_specifications];
 8 end <package_name>;
 9
10 package body <package_name> is
11         [subprogram_bodies];
12         [internal_subprogram_declarations];
13         [internal_constant_declaration];
14         [internal_type_declaration];
15 end <package_name>;

Como puede observarse en la parte declarativa SOLO las declaraciones son permitidas. Si es necesario la parte descriptiva de la declaración debe ser hecha en el cuerpo (body) del paquete.
En un blog anterior detallé la inferencia de una memoria ROM a partir del código genérico VHDL . En ese blog destacaba el uso de paquete para definir los valores de la ROM, sobre todo en casos que el tamaño de la memoria sea grande. En concreto, la declaración del paquete para este caso en particular sería la siguiente:

 1 library ieee;
 2 use ieee.std_logic_1164.all;
 3 use ieee.numeric_std.all;
 4
 5 package pkg_rom is
 6
 7  constant data_length : natural := 16;
 8  constant addr_length : natural := 10;
 9  constant mem_size    : natural := 2**addr_length;
10  subtype rom_word is std_logic_vector(data_length-1 downto 0);
11  type mem_type is array (mem_size-1 downto 0) of rom_word;
12
13  constant mem : mem_type :=
14        (0 => x"abcd", 1 => x"beef", 2 => x"5555",3 => x"1010",
15         4 => x"5a6b", 5 => x"f0f0", 6 => x"1234",7 => x"fabc",
16         8 => x"2345", 9 => x"9876", 10=> x"5432",11=> x"6666",
17        12 => x"0101",
18        13 => std_logic_vector(to_unsigned (1234,16)),
19        others => x"4247");
20
21 end package pkg_rom;

Como se ve es exactamente igual a las declaraciones realizadas en la parte declarativa de la arquitectura del componente sync_rom, detallado en el blog ROM.
Ahora, cómo se usa un paquete???? . . . . . .
En primer lugar el paquete en sí debe ser guardado con una extensión .vhd e importado en el proyecto que se está trabajando como si fuera un componente más del proyecto. En segundo lugar, debemos decirle a la herramienta de síntesis que se quiere hacer uso del paquete dentro de la sintesis del componente que se está sintetizando. En tercer lugar se usan las declaraciones del paquete dentro del componente como si  se hubieran declarado dentro del mismo componente. Mejor vemos todo esto siguiendo el ejemplo con el que estamos trabajando.
Las siguientes lineas de código detallan el uso del paquete previamente definido en un componente.

 1 library ieee;
 2 use ieee.std_logic_1164.all;
 3 use ieee.numeric_std.all;
 4 use work.pkg_rom.all;
 5
 6 entity sync_rom is
 7    port (
 8      clk     :in  std_logic;
 9      address :in  std_logic_vector(addr_length-1 downto 0);
10      data_out:out std_logic_vector(data_length-1 downto 0)
11           );
12 end sync_rom;
13
14 architecture synth of sync_rom is
15
16 begin
17    rom_proc : process (clk)
18    begin
19       if rising_edge(clk) then
20          data_out <=mem(to_integer(unsigned(address)));
21        end if;
22    end process rom_proc;
23
24 end architecture synth;

Analizando el código vemos que línea 4 es la que le dice a la herramienta de síntesis que existe un paquete, que es llamado pkg_rom, y que de ese paquete se usará todo (all). El uso de la librería work, se debe al hecho que es la librería que por defecto usan las herramientas de síntesis para buscar lo que no encuentran en el directorio de trabajo, esta librería en realidad es creada automáticamente por la herramienta de síntesis. En caso de tener una librería propia, creada por uno mismo, se debe reemplazar work por el nombre de la librería creada; en este caso la librería la debe crear uno mismo (concepto que abordaré más adelante en otro blog). Por otro lado, el uso de all, se refiere que se desea que la herramienta de síntesis compile e importe en el presente proyecto TODO, (all) lo que contiene el paquete. En casos de paquetes muuuy grandes, se puede reemplazar all por la función o declaración específica que se desea del paquete (de este modo se compila e importa solo lo que se necesita).
En el resto del código se hace uso de las constantes y tipos declaradas en el paquete sin ningún tipo de problemas.
Para completar lo explicado en este blog con respecto a paquetes, deberías ver el blog de funciones (functions) en el cual se detalla no solo la parte declarativa del paquete sino también la descripción funcional en el cuerpo del paquete (package body).
Bueno, espero haya sido de utilidad.... y a no tener miedo de usar paquetes !

viernes, 3 de junio de 2011

Memorias ROM: FPGA-VHDL, Cómo? ? ? ?

Básicamente hay dos formas de que una memoria ROM sea implementada en un FPGA a partir del código VHDL:
- Deduciendo la memoria desde el código VHDL. Comúnmente conocido como Inferir.
- Generando la memoria a partir de la herramienta de generación de IPs ofrecida por el vendedor del FPGA (CoreGen, MegaWizard, etc).
Por portabilidad y code-resuse el primer método, inferir, es el mas aconsejado. Pero por supuesto tambien se puede recurrir a los IPs del FPGA (segundo caso).
A su vez dentro del metodo de inferir hay diferentes opciones para el modo de 'cargar' la ROM:
1- declarando un tipo (type), luego una señal de ese tipo y asignarle a la señal valores constantes (datos de la ROM)
2- declarando los datos de la ROM en un archivo (data file) y usar instrucciones de apertura y lectura de archivos  para leer los datos.
3- usando instrucción 'case'.

Analizando las distintas opciones en gral podemos decir el caso 3 se usa cdo la memoria es pequeña, de lo contrario queda un 'case' tremendamente largo y un poco anti-estético. El caso 2 depende del entorno del proyecto- hay empresas en las que no les gusta o no están de acuerdo con el acceso a disco para leer/escribir datos, otras que directamente lo prohíben y otras que lo estimulan. Por lo dicho entonces, detallaré la metodología de la opción 1 que es la mas generalizada.

El primer paso es la declaración del arreglo bi-dimensional que fija el tamaño de la memoria. Pero, antes de detallar esta declaración es conveniente declarar también dos constantes: una constante relacionada con el ancho del bus de direcciones de la memoria, y otra constante relacionada con el ancho del bus de datos de la memoria. Conviene que estas declaraciones sean realizadas como generics dentro del componente, así es más fácil la modificación del tamaño de la misma.
De acuerdo a lo descrito, la primera parte del código de la memoria ROM quedaría:

1 library ieee;
2 use ieee.std_logic_1164.all;
3 use ieee.numeric_std.all;
4
5 entity sync_rom is
6  generic (data_width  : natural := 16;
7           addr_length : natural := 10);
8  port (
9   clk     :in  std_logic;
10  address :in  std_logic_vector(addr_length-1 downto 0);
11  data_out:out std_logic_vector(data_width-1  downto 0)
12     );
13 end sync_rom;

En al parte declarativa de la arquitectura se hacen las declaraciones de: 
- constante del tamaño de la memoria basado en el generic que declara en ancho del bus de direcciones (línea 2).
- type que declara el tamaño del arreglo bidimensional usando la constante recién declarada y el ancho del bus de datos declarado como generic (líneas 3-4). 
- constante que determina los distintos valores asignados a cada dirección de la memoria ROM (líneas 5-11). 
Por último, en la parte de descripción funcional del componente, se describe el comportamiento de la memoria ROM. Sencillamente, a la señal dato de salida se le asigna el correspondiente valor de la memoria de acuerdo del valor del bus de direcciones. Dos aclaraciones con respecto a la descripción del comportamiento de la memoria: 
1- en este caso se describe en un proceso sincrónico (líneas 13-18). Razón?... esto es debido a que por ejempo para implementar la memoria ROM en un BRAM de los FPGAs de Xilinx, la memoria DEBE SER descripta de modo sincrónico; de lo contrario es implementada en LUTs. Lógicamente que nadie quiere que una gran memoria sea implementada en LUTs teniendo los BRAMs disponibles. En otros casos se puede directamente hacer una descripción combinacional de la memoria, que directamente sería solo la instrucción de asignación detallada en línea 16.
2- la conversión de 'address' primero a 'unsigned' usando cast unsigned, y luego de unsigned a entero (integer) usando la función to_integer. Con estas conversiones lo que se hace es encontrar el índice de valor entero del arreglo definido como constante. De este modo se accede al valor asignado a esa dirección y que a su vez es asignado a la señal en el RHS de la instrucción (Right Hand Side).  
El código de la parte de la arquitectura de la memoria es la siguiente:  

 1 architecture synth of sync_rom

 2 constant mem_size : natural := 2**addr_length;
 3 type mem_type is array (mem_size-1 downto 0) of
 4            std_logic_vector (data_width-1 downto 0);
 5 constant mem : mem_type :=
 6  (0=> x"abcd", 1=> x"beef", 2 => x"5555", 3 => x"1010",
 7   4=> x"5a6b", 5=> x"f0f0", 6 => x"1234", 7 => x"fabc",
 8   8=> x"2345", 9=> x"9876", 10=> x"5432", 11=> x"6666",
 9  12=> x"0101",
10  13=> std_logic_vector(to_unsigned (1234,16)),
11  others => x"4247");

12 begin
13    rom : process (clk)
14    begin
15       if rising_edge(clk) then
16          data_out <=mem(to_integer(unsigned(address)));
17       end if;
18    end process rom;

19 end architecture synth;

Línea 10 ejemplifica otro modo de declarar el valor constante del dato correspondiente a una dirección (se puede usar cdo estamos lazzy y queremos evitar el cálculo de entero a slv :) ).

En caso que el tamaño de la ROM sea muy grande, el tamaño de la declaración de valores del arreglo bidimensional crecerá proporcionalmente y puede llegar a ser de varias hojas de valores,  perdiéndose el código VHDL descriptivo de la memoria. Para estos casos lo mejor es declarar las constantes y el arreglo de la memoria en un paquete. Así, el código VHDL es más compacto y el paquete 'oculta' la longitud de la memoria.

Consideraciones de Síntesis
La implementación de la memoria ROM en el FPGA puede realizarse en:
- LUTs.
- Bloques dedicados de Memoria RAM.
En qué bloque se implementaría la memoria descrita anteriormente????... La implementación depende de varios factores, entre ellos principalmente el tamaño de la memoria (1), pero depende también del 'seteo' de la herramienta de síntesis (2), por último si se usan o no atributos al respecto (3). Veamos:
1- Las herramientas de síntesis tienen un 'umbral de tamaño'. Si el tamaño de la memoria supera ese umbral la memoria implementará en BRAM, y sino supera el umbral se implementará en LUTs. Este umbral depende del tamaño del dispositivo FPGA que se esté usando, de la profundidad de la memoria y del número total de bits de memoria. Por ejemplo en la documentación de Xilinx se puede encontrar la siguiente tabla que muestra los valores de umbral mínimos para implementar memoria en los BRAM:







































Sin embargo, y esto es importante!, se puede 'obligar' a la herramienta de síntesis a forzar la implementación ya sea en LUTs o Bloques dedicados de RAM, dejando sin efecto los valores detallados en la tabla anterior. Para ello se tienen las dos opciones siguientes:
2- Variar el 'seteo' de la herramienta de síntesis. Por defecto el seteo para la implementación de memorias es 'AUTO', esto quiere decir que dependiendo del tamaño será donde se implementará la memoria. Cambiando este valor a BRAM o DISTRIBUITED se logra forzar a la herramienta de síntesis  a implementar la memoria donde uno desee.
3- Usar atributos en el código fuente VHDL o Verilog. Para ello, primero se debe declarar el atributo de síntesis respectivo como tipo 'string'. Para el caso de Xilinx XST, el atributo se denomina 'rom_style', para Altera Quartus 'romstyle', y para Synplify 'syn_romstyle'. Una vez declarado el atributo es necesario asociarlo con la señal o variable que representa la memoria ROM que se desea inferir e implementar. Al hacer la asociación se fija también el tipo de memoria que se desea usar para la implementacion de la misma. Este valor depende de la herramienta de sintesis, asi por ejemplo para Quartus los valores permitidos son: "logic", "M512", "M4K", M9K", M144K", "MLAB" o "M_RAM". Para el caso de XST, los valores permitidos son "auto", "block" o "distributed". Finalmente para Synplify (syn_romstyle), los valores permitidos son 'register' o 'block_rom'. 
A continuación un ejemplo del uso de atributos de síntesis detallado en las siguientes líneas de código:

-- Xilinx XST
attribute rom_style : string;
2 attribute rom_style of data_out:signal is "block";
-- Altera Quartus
attribute romstyle : string;
attribute romstyle of data_out:signal is "M_RAM";
-- Synplify
attribute syn_romstyle : string;
attribute syn_romstyle of data_out:signal is "block_rom";                            


Estas lineas de código deberían ir a continuación de la línea 11 (del ejemplo anterior) en la parte declarativa de la arquitectura. Para el caso de XST en esta lineas se asigna el atributo 'rom_style" a la señal 'data_out' (salida de la ROM) con el valor 'block'. De este modo la ROM se implementará en un BRAM sin importar su tamaño ni el seteo de XST. El otro valor que se le puede asigna al atributo 'rom_style' es 'distributed', el cual hará que la memoria se implemente en lógica distribuida, es decir en LUTs.


Versión pdf?  Hacé click acá :)