lunes, 10 de diciembre de 2012

Personalizando el uso de ISim

Introducción


El simulador ISim puede fácilmente ser personalizado a fin de facilitar las tareas de visualización y depuración del diseño bajo test.

Procedimiento


Ejecutar el test bench como normalmente lo ejecuta.
Modifique las señales mostradas en el visualizador de formas de ondas de acuerdo a lo que necesite. Algunas de las opciones que brinda ISim son las siguientes:
  • Mover las señales a fin de juntarlas por funcionalidad. Seleccione la señal a ser movida con un simple click sobre la señal, luego mantenga el botón izquierdo de mouse apretado y arrastre la señal hacia arriba o hacia abajo.
  • Cambiar de color de la forma de onda. ISim ofrece la opción de cambio de color de la forma de onda de cada señal. Para ello simple click en la señal a cambiar el color, para seleccionarla, y luego presione botón derecho, y seleccione Signal Color, y luego el color que quiera usar para esa señal en particular. Este cambio de color es muy útil para señales que en cierto modo son de referencia para otras señales, pudiendo ser identificadas fácilmente en el conjunto de todas las señales.
  • Inserción de divisor de grupo de señales. Una vez realizado el paso 1, es conveniente colocar un titulo que de alguna idea de la funcionalidad de las señales. Para ello coloque el icono del mouse donde desea colocar el titulo del grupo de señales, presione botón derecho del mouse, y seleccione New Divider, y escriba el titulo correspondiente. Puede colocar tantos divisores como necesite.
  • Cambio de número relacionado a buses. Por cada bus se puede  seleccionar distinta representación del número correspondiente. Por ejemplo, se puede seleccionar Hexadecimal, Decimal, etc.
  • New Virtual Bus, permite juntar señales y formar un bus que solo es válido en la simulación.

Una vez realizados todos los cambios que desee, grabe esa configuración haciendo File->Save as. Escriba el nombre correspondiente, y se le agrega la extensión .wcfg.
Ahora bien, para usar esa configuración cada vez que se invoque el simulador debe hacerle saber al simulador que use esa configuración ya grabada en vez de la que usa por defecto. Seleccione Simulate Behavioral Model, y luego presionando botón derecho seleccione Process Properties. Tal como se ve en Figura 1.




Figura 1 - Acceso a las opciones del simulador

Una vez abierta la ventana de propiedades del ISim, haga clikc en la opción Use Custom Waveform Configuration File, y luego en la opción Custom Waveform Configuration File, navegue para agregar el archivo grabado en el paso anterior, tal como se puede observar en Figura 2.

Figura 2 - Configuración del simulador para usar la plantilla creada

A partir de ahora cada vez que se invoque ISim, se abrirá con la configuración grabada. 

Útil? te sirve de algo este blog? Avisame !!!  :) 

domingo, 28 de octubre de 2012

Replicación de Lógica - Inserción de Buffer - Alta Cargabilidad (High Fan-Out)

Introducción

Entre las tantas tareas que llave a cabo la herramienta de síntesis, hay una que se encarga de revisar la cargabilidad (fan-out) de cada señal lógica.Por defecto existe un número límite máximo de cargabilidad permitido. Este numero básicamente depende de la familia del FPGA con que se este trabajando. Por supuesto existe la opción de incrementar ese número, aunque no es lo mas aconsejable. La mejor solución para casos en que la cargabilidad de una señal sea mayor a la permitida, es lo que técnicamente se llama replicación de la lógica que genera la señal de tan alta cargabilidad. La cargablidad de cada registro, compuerta lógica y cualquier otro elemento lógico es analizado por el compilador de la herramienta de síntesis. Si el numero de cargabilidad encontrado supera al establecido por el diseñador, el compilar replica el nodo fuente de la señal hasta que el número de cada copia esté por debajo del número máximo de cargabilidad permitido. 

Cargabilidad (Fan-out)

Fan-out es definido como el número máximo de entradas de componentes lógicos que pueden ser conectadas con la salida de otro componente lógico. De acuerdo al diseño o circuito que se implementa algunas señales internas del mismo (salidas) se conectan a las entradas de otros componentes. Esta cantidad de conexiones puede ser un número desde 1 hasta un numero muy alto (por ej. 10000), este es el números que se conoce como fan-out. No hace falta que uno sea el que lleva la cuenta de la cargabilidad de cada señal, todas las herramientas de síntesis detallan en su reporte las señales que tienen un alto fan-out. 
Por qué existe un número máximo de fan-out? El número conexiones máximas están limitadas,  por un lado, por la degradación que puede tener la señal de del alta cargabilidad debido a que la principal carga que ve la salida es la capacitancia de entrada de los componentes a los que está conectada; así mientras mas conexiones tiene la salida, más capacitancia, más tarda en subir (rise-time) y en bajar (fall-time) la señal. Al tardar mas en subir y en bajar, la señal sufre un retardo generado solo por la cargabilidad, de este modo el rendimiento del sistema puede verse reducido. Finalmente, en el caso de los FPGAs, otro problema de una señal con un número alto de fan-out es que crea dificultades para rutear la señal dentro del FPGA, a no ser que se use una ruta dedicada para la señal de alta cargabilidad. 
Normalmente las herramientas de síntesis de los distintos fabricantes tienen un valor de fan-out para todas las señal (valor global) configurado por defecto. Sin embargo, este valor puede ser sobrescrito usando ya sea restricciones en el archivo correspondiente, (.sdc para Altera; .ucf para Xilinx), o usando atributos de síntesis, cuyo nombre depende de la herramienta de síntesis que se use.  


En el caso de Synplify, la herramienta permite configurar el valor de fanout máximo desde una ventana de configuración: 
Project -> Implementation Options -> Device

en la opción Fanout Guide se muestra el numero máximo de fanout.




El valor configurado para fanout, se aplica a TODAS las señales del diseño, se por ello se lo denomina fanout global. Tal como se explico anteriormente este valor no puede ser demasiado alto, pero tampoco demasiado bajo. En este último caso se corre el riesgo que la herramienta de sintesis duplique lógica o inserte buffers sin realmente necesitarse. Ambos extremos afectan el ruteo y el rendimiento del diseño. 
Para sobrescribir el fnaout global e imponer un fanout distinto en un mas bajo nivel se puede usar el atributo:

syn_maxfan 

Este atributo se aplica a distintos niveles de jerarquía del diseño, descripto en el siguiente cuadro: 


Cuál es la diferencia entre límite duro y blando? 
En el limite blando la herramienta puede o no puede replicar la logica o introducir un buffer, una vez superado el valor límite. En el caso de aplicar syn_maxfan a un registro por ejemplo, si se supera el valor de fanout configurado por el atributo SI o SI la lógica es replicada o se agrega un buffer a la señal.  
A continuación se presenta un ejemplo de un valor alto de fanout que afecta el rendimiento máximo del sistema. Como puede apreciarse el retardo de ruteo es excesivamente largo, principalmente debido a un muy alto valor de fan-ou (seguramente también esta señal no esta usando un ruteo dedicado). 


Esta señal, sig, es un típico caso en el que un alto fanout está afectando la frecuencia máxima de funcionamiento del sistema, por lo que se debería replicar la lógica generada de esa señal.

Quartus Configuración y Atributo

El atributo que controla el valor de fanout máximo en la herramienta de síntesis del Quartus II maxfan
El valor global de fanout se configura en en el Assignment Editor

XST Configuración y Atributo

El atributo que controla el valor de fanout máximo en la herramienta de síntesis XST del ISE de Xilinx se denomina max_fanout
El valor global de fanout se configura en: 
Process -> Properties -> Xilinx Specific Options -> Max Fanout

Detalles Lógicos

La herramienta de síntesis a fin de cumplir con los requisitos del valor máximo de fanout, puede hacer dos cosas: 
- replicación de lógica
- introducción de un buffer

 

Replicacion de Lógica

Cuando el fanout de un registro o una lógica combinacional supera el valor configurado, la herramienta de síntesis reduce el fanout al replicar la lógica de salida, ya sea secuencial o combinacional.

 

Introducción de Buffer

Un buffer es introducido por la herramienta de síntesis cuando una señal de entrada al FPGA tiene un alto fanout. 

 

Replicación Manual

Teniendo en cuenta que no todas las señales con alta cargabilidad originan problemas de bajo rendimiento, se puede proceder a una replicación manual del componente con una alta cargabilidad. Para ello en el código VHDL o Verilog se describe el componente las veces que sea necesario. Un punto muy importante en estos casos, es el uso de atributos de síntesis para prevenir que la lógica replicada sea optimizada por la herramienta de síntesis. Para ello se usan atributos tales como syn_preserve, syn_keep (en el caso de usar Synplify).


Excepciones

Como siempre ocurre hay excepciones a estas reglas y algunas son las siguientes:
  • Se puede 'exigir' a la herramienta de síntesis que use un 'buffer' en lugar de replicar la lógica. En el caso de Synplify esto se logra configurando el atributo syn_replicate a valor lógico '0', ya sea globalmente, o sobre el modulo o sobre un registro en particular. Este atributo previene la replicación, por lo que el software usará buffers para evitar los problemas relacionados con un fanout muy elevado
  • Para especificar que un puerto de entrada, por ejemplo una entrada de reloj, no sea usada con un buffer, se puede usar el atributo syn_noclockbuf, sobre esa entrada. Esto se usa comúnmente cuando se esta muy al limite con el numero de buffers globales y se necesita el buffer para una senal que no tiene un alto fanout pero que tiene una restricción de tiempo muy critica. 
  • Para casos en que el rendimiento del sistema no se muy crítico, es decir cuando la frecuencia de trabajo del sistema es baja o muy baja, se puede directamente evitar cualquier replicación de lógica o introducción de buffer al configurar el valor de fanout a un numero muy alto.  
  • Otros atributos que se pueden usar para que la lógica no sea replicada y en su lugar se introduzca un buffer, son los atributos syn_keep y syn_preserve. syn_keep es usado cuando se quiere mantener una señal, mientras que syn_preserve se utiliza para preservar un registro. En ambos casos si se supera el numero máximo de fanout, un buffer sera introducido en lugar de replicar la lógica.
  •  

Conclusión 

Es importante resaltar que las señales con alta cargabilidad en un diseño  no deberían ser tocadas a menos que esté en riesgo el rendimiento del diseño, es decir que la técnicas detalladas mas arriba se deberían usar en caso que la señal que tiene alta cargabilidad este afectando la frecuencia máxima a la que se desea funciones el diseño
La cargabilidad de las señales es detallada en los distintos reportes presentados por la herramientas de síntesis, ya sea Synplify, Quartus, XST, etc. En función de ese número y de que si se trata de una señal que es crítica para el máximo rendimiento del diseño, es que se deben aplicar las técnicas presentadas en este blog, para sea replicar la lógica o insertar un buffer.

martes, 2 de octubre de 2012

Inicialización y Asignación de Vectores - Aggregate

 Introducción

Un punto de mucha importancia cuando se escribe código VHDL es la escritura en forma parametrizada del mismo. Esto significa usar los distintos atributos e instrucciones de VHDL a fin de evitar hacer uso de referencias fijas las que deberán ser cambiadas si se modifica el tamaño de algún dato o señal del código escrito. Así, las referencias a tamaños de un dato o señal deberían ser expresadas en términos de atributos de VHDL o parámetros definidos por el diseñador. 

Aspectos Prácticos

Para simplificar los ejemplos comenzaremos con la inicialización de una señal. La asignación de un valor a una señal de tipo arreglo (array) normalmente se hace de la siguiente manera: 

data_out <= "00000000";

Si bien esa asignación es correcta, no es la más adecuada si se quiere escribir el código de forma parametrizada. La misma función cumple la siguiente asignación conocida como aggregate: 

data_out <= (others => '0');

Que también se puede usar para inicializar a todos ‘1’:

data_out <= (others => '1');

Similarmente se puede escribir: 

data_out <= ( 0 => ‘1, 3 => '1, others => ‘0);

Para formar de este modo el dato “00001010”. 
Un error común cuando se escribe código parametrizado es que un aggregate no puede ser usado en una expresión y debe usarse con un objeto de tamaño conocido. Así, el siguiente código dará un error el ejecutarse: 

signal internal_bus: std_logic_vector(width-1 downto 0);
 . . .
output_bus <= x"FF" when (internal_bus=(others=>0)) else . . .


En este comparación el tamaño de internal_bus es conocido, pero no el tamaño del dato con quien se esta comparando internal_bus. Para solucionar este problema se usa el atributo ‘range para proveer el tamaño del objeto. 

output_bus <= x"FF" when (internal_bus=(internal_bus’range=>0)) else . . .

Otro manera de hacer algo similar a lo detallado en el ejemplo anterior, sería mediante el uso de una constante tal como se detalla a continuación: 

constant all_zero: std_logic_vector(width-1 downto 0) := (others => ‘0);

signal internal_bus: std_logic_vector (width-1 downto 0);
. . .

output_bus <= x"FF" when (internal_bus = all_zero) else . . .

Una nota con respecto al uso de aggregate, es que cuando se usa en un arreglo compuesto por solo un elemento, la asociación por nombre debe ser usada para especificar el valor. Por ejemplo: 

constant valor_ini: std_logic_vector(2 downto 2):= (2=>); -- correcto

constant valor_final: std_logic_vector(2 downto 2):= (0); -- incorrecto  

Finalmente quiero recordarles que un aggregate también se usa para asignar valores de señales a otra señal en particular. Ejemplo: 

signal a, b, c, d: std_logic;
signal tmp: std_logic_vector(3 downto 0);

 .. .

tmp <= (a, b, c, d);

De este modo tmp(3) toma el valor de a, tmp(2) el de b, tmp(1) el de c y tmp(0) el de d. En caso de querer cambiar el orden se lo debe especificar en la asignación, por ejemplo: 

tmp <= (3 => c, 2 => a, 1 => d, 0 => b);

Asi, tmp tiene los siguientes vlaores asignados (c, a, d, b).
Tambien se puede expresar un aggregate de la siguiente manera: 

tmp <= (3 => ‘1, 2 | 1 | 0 => ‘0);

tmp <= (3 => ‘1, (2 downto 0) => ‘0);

tmp <= (3=> ’1, others => ‘0);

tmp <= (1, ‘0, ‘0, ‘0, ‘0);  


Todas estas ultimas cuatro asignaciones son equivalentes.

viernes, 6 de julio de 2012

Lazos Combinacionales


Introduccion

Lazos combinacionales son estructuras lógicas que contienen realimentación sin ningún elemento sincrónico en el camino. Normalmente los lazos combinacionales provocan inestabilidad y sistemas pocos confiables, violando los principios de diseño sincrónico al establecer un lazo de realimentación sin registros. 

Porqué? Cómo se genera un lazo combinacional?

Un lazo combinacional es implementado en hardware cuando en el código VHDL escrito una señal que está del lado izquierdo de una instrucción de asignación (a la izquierda del símbolo <=) también aparece en la expresión aritmética/lógica del lado derecho de la instrucción de asignación (a la derecha de <=); siempre y cuando se esté describiendo lógica combinacional. Por ejemplo las siguientes líneas de código generaran un lazo combinacional si se escribe en un proceso combinacional o directamente como una instrucción de asignación concurrente.
  
1 acc <= acc + data;
2
3 Z <= Z nand B;
4
5 cnt <= cnt + 1;

 

Sin embargo es importante, muy importante, aclarar que si estas mismas líneas de código son escritas dentro de un proceso controlado por reloj, se generara la respectiva lógica secuencial; debido a que el reloj del proceso almacena el valor correspondiente por un ciclo de reloj, de este modo no existe una realimentación combinacional.

Hardware

La siguiente figura representa un esquema de un lazo combinacional:



Como se ve en la figura, la salida de la lógica combinacional se realimenta a si misma sin ningún tipo de registro en el medio. Este tipo de esquema lógico, normalmente no se desea, no es lo que se desea implementar, por ello la herramienta de síntesis genera una advertencia (warning) tal como se detalla en el siguiente ejemplo.

 1 library ieee;
 2 use ieee.std_logic_1164.all;
 3
 4 entity lazo_comb is
 5   port(
 6     a: in  std_logic;
 7     z: out std_logic);
 8 end lazo_comb;
 9
10 architecture beh of lazo_comb is
11
12  signal y: std_logic;
13
14 begin
15     z <= y; 
16
17 process(a,y)
18  begin
19     y <= y nand a;
20 end process;
21
22 end beh;

La herramienta de síntesis, Synplify en este ejemplo, genera el siguiente mensaje de advertencia para este código:


El mensaje ‘found combinational loop at y’ significa que la señnal ‘y’ es realimentada combinacionalmente tal como se puede apreciar en el diagrama RTL de la respectiva implementación del código descrito:


  
A continuación se puede ver la simulación del sistema descrito arriba.



                Esta figura merece un detallado análisis. En primer lugar la primer ventana (de arriba hacia abajo) grafica las formas de ondas de las señales del sistema cuya principal expresión es la de la línea 24 de la segunda ventana. La tercer ventana, Transcript, detalla un error de simulación, diciendo que el limite de iteraciones del simulador fue alcanzado a los 50ns y no se llego a ningún valor estable. Es decir que el sistema empezó a oscilar y se quedo oscilando. El numero de iteración limite es programable en ModelSim (Simulate->Runtime Options), y en la mayoría de los simuladores. Por defecto este valor es de 5000. Viendo mas en detalle la parte inferior de la ventana donde se grafican las formas de ondas, se puede leer que el numero de Delta alcanzado es de 5000, que es justamente el numero limite de iteraciones configurado. Han transcurridos 5000 Deltas y aun el sistema no es estable.  
Otro punto importante a considerar en este ejemplo es el hecho de lo importante que es simular un sistema aun cuando sea muy simple. Suponiendo que hubiéramos directamente configurado el FPGA sin haber realizado la simulación (ya que la herramienta de síntesis nos da solo un ‘warning’), hubiéramos visto que la salida del sistema no era estable; y hubiéramos perdido una considerable cantidad de tiempo tratando de ver porque la salida no es estable
         En diseños con una gran cantidad de código a veces es muy fácil cometer errores como el del código descrito arriba. Por ello, hay que seguir un cierto orden en la escritura del código, tratando de mantener un cierto flujo de datos, por ej. de derecha a izquierda.
            En casos en que deliberadamente se desea implementar cierta lógica con un lazo combinacional tener en cuenta:
  • Comentar suficientemente el código de modo que se puede fehacientemente conocer la razón de existencia del lazo. 
  •  Realizar todas las simulaciones posibles, primero en PC y luego en hardware para comprobar que aun con la existencia del lazo el sistema sigue funcionando correctamente.
       Otro punto importante a tener en cuenta cuando deliberadamente se implementa un lazo combinacional, es que la herramientas de análisis de tiempo estático (Static Taming analysys, STA) normalmente incrementan por un valor (2-8 veces) el periodo mínimo cuando encuentran un lazo combinacional. Por ello, en estos casos se debería decirle a la herramienta STA que ‘ignore’ ese camino en particular. La sintaxis para el caso de Quartus (Altera), es ‘set_false_path’ (recordar que las herramientas de Altera usan constraints con las sintaxis de Synopsys; para ISE (Xilinx) use TIG con su respectiva sintaxis.


domingo, 13 de mayo de 2012

Generación de Estímulos y Verificación de Respuestas en Test Bench

Introducción

En este blog describiré lo relacionado a generación de estímulos y verificación de datos en un test bench. NO se describirá lo que es en sí un test bench ni su forma de escribirlo, ya hay mucho de esto en internet, sin embargo hay poca información de cómo generar los datos/relojes/resets para estimular el dispositivo bajo test (usualmente conocido como DUT, device under test).
Básicamente un test bench sirve de estímulo y verificación de datos del componente o dispositivo que se desea verificar. El comportamiento del componente bajo el estímulo definido en el test bench puede verse en una pantalla del simulador usado a tal fin o puede verificarse con el comportamiento esperado usando instrucciones a tal fin en el mismo test bench o también puede guardar los datos resultados en archivos (este último caso no será detallado en este blog). Un diagrama ilustrativo de lo dicho se aprecia en la siguiente figura.



Dentro del código VHDL del test bench podemos encontrar como mínimo los siguientes procesos:
  • Generación de datos de estímulo para el DUT
  • Generación de reloj/es de estímulo para el DUT
  • Generación de señales de inicialización para el DUT
  • Verificación de datos proveniente del DUT bajo un estímulo determinado

Generación de Datos de Estímulo para el DUT

En cualquier sistema que se desee verificar se va necesitar generar datos que simulen el comportamiento de las entradas al DUT. De este modo se deberá, de algún modo, describir en VHDL el comportamiento de estas entradas. Este comportamiento debe estar basado en las especificaciones del sistema que se desea verificar; es decir hay que leer bien las especificaciones del diseño para saber de antemano cual es el comportamiento esperado de las entradas, para luego poder describir ese comportamiento en VHDL/Verilog. Por supuesto, y esto es importante, TAMBIÉN se debe verificar el comportamiento del DUT ante comportamiento no esperado de las entradas.
Se describen a continuación diferentes modos de generar distintos valores para ser asignados a las entradas del DUT.

Generación de Datos I
El siguiente código va cambiando los valores de entrada en el flanco de bajada de reloj. Los valores X1, y X2 son asignados a DATA1 y DATA2 en el flanco negativo del reloj. 


Generación de Datos II
Se pueden generar datos en función del tiempo de simulación. Existen dos modos de hacerlo uno es la generación usando tiempo relativos y la otra es la generación usando tiempos absolutos.
Tiempo Relativo: se van asignando valores a las señales en los tiempos de simulación con respecto al tiempo previo, de una manera acumulada.


Tiempo Absoluto: se asignan valores a las señales en los tiempos absolutos de simulación con respecto al momento del comienzo de la simulación.

  
Generación de Datos Usando Arreglos
Cuando se desea usar como estímulo ciertos valores de datos, los cuales van a ir cambiando en determinada secuencia de tiempo, el uso de arreglos de datos es bastante común, tal como se detalla en el siguiente ejemplo.


 1 architecture array_usage of in_test_benches is
 2
 3    signal Add_Bus : std_logic_vector(7 downto 0);
 4    type stimulus is array (0 to 4) of std_logic_vector (7 downto 0);
 5     
 6    constant DATA : stimulus :=
 7     ("00000000",    -- declara el estimulo
 8      "00000001",    -- como un array.
 9      "00000010",    -- Estos datos seran
10      "00000011",    -- usados para estimular
11      "00000100");   -- las entradas
12
13 begin
14      stim_proc: process
15      begin
16        for i in 0 to 4 loop     -- for-loop que asigna  
17          Add_BUS <= DATA(i); -- a ‘Add_Bus’ un nuevo valor
18           wait for 250 ns;    -- de estimulo cada 10ns
19        end loop;
20      wait;  
21   end process stim_proc;
22 . . .
23 end array_usage;


En el caso descrito en el código arriba, cada dato del arreglo DATA, definido como un arreglo de 5 datos de 8 bits cada uno, es asignado a la señal Add_BUS cada 10 nanosegundos. Este tipo de asignación es totalmente asincrónica, no señal de reloj es usada para nada.
Para una generación de estímulos sincrónica, usando el reloj del sistema bajo test, se puede usar el siguiente código.


 1 architecture array_usage of in_test_benches is
 2   
 3 -- mismas declaracion que el ejemplo anterior
 4
 5 begin
 6   stim_proc: process
 7   begin
 8      for i in 0 to 4 loop
 9        Add_BUS <= DATA(i);
10          for k in 1 to 7 loop
11             wait until rising_edge(clk);
12          end loop;
13        wait until falling_edge(clk);
14      end loop;
15      wait;
16   end process
stim_proc;
17 end array_usage;


En este caso, cada dato del arreglo es asignado a Add_BUS en el flanco de bajada del reloj cada siete flancos de subida del reloj. 

Generación de Reset
La generación de la señal de reset en bastante sencilla, ya que no es una senal que este cambiando valores bastante seguido durante el tiempo de simulación. Un ejemplo se muestra en las siguientes líneas de código.


 1 -- reset totalmente asincronico
 2 reset_proc: process
 3 begin
 4   rst <= '1';
 5   wait for 23 ns;
 6   rst <= '0';
 7   wait for 142 ns;
 8   rst <= '1';
 9   wait for 54 ns;
10   rst <= '0';
11   wait;
12 end process reset_proc;


Tal como se detalla en el código, en este caso se genera un reset asincrónico en la activación (assert) y en la desactivación (desassert). También se destaca la activación del reset en el ‘medio’ de la simulación, no olvidarse de este pequeño detalle que puede hacernos ver algún problema de inicialización cuando el sistema está en funcionamiento normal.

Como ya expliqué en un blog anterior, en realidad es conveniente que un sistema tenga un reset que se active asincrónicamente y se desactive sincrónicamente. Aquí el código del mismo. 



1 -- desactivacion sincronica de reset
2 sreset_proc: process
3 begin
4   rst <= ’1;
5   for i in 1 to 5 loop
6     wait until clk = ‘1;
7   end loop;
8   rst <= ’0;
9 end process sreset_proc;




Verificación de Datos

A fin de contrastar el valor obtenido como salida del DUT ante un cierto estímulo, se usa comúnmente la instrucción assert. De este modo se compara el valor esperado con el obtenido, y dependiendo del resultado de la comparación se lleva a cabo un reporte y se detiene o no la simulación.
 En los test bench la instrucción assert también es usada para comprobar restricciones de tiempo del diseño, pero ese es otro tema y en nuestro caso solo la usaremos para verificación de datos.

La sintáxis de la instrucción assert es bastante sencilla: si la expresión booleana de la instrucción assert es falsa se ejecuta lo que está dentro del assert; normalmente es un cierto reporte, que debería detallar el motivo de la ejecución del assert, y un cierto grado de severidad que indica cuán importante es el error detectado con el assert. Un ejemplo de uso es el siguiente: 



1 assert (out1 = "0110")
2     report"Valor esperad de salida out1 no es igual a 0110"
3       severity ERROR;


En este caso se desea saber si en cierto instante de la simulación la señal out1 tiene el valor “0110”. Si la igualdad es falsa, es decir out1 no es igual a “0110”, se ejecuta el assert reportando el mensaje “El valor de out no es igual a 0110”, y el nivel de severidad asociado es ERROR. En caso de que el problema detectado sea muy grave se puede detener directamente la simulación usando como grado de severidad, severity, FAILURE

Un importante punto para usar el assert con una condición para saber si es correcto el resultado del DUT ante determinado estimulo, es saber en que momento de la simulación, o mejor en que tiempo de simulación se está preguntando por la condición deseada. Para ello se debería hacer un detallado diagrama de tiempo de los estímulos, y en función del tiempo de simulación de determinado estimulo hacer la pregunta del assert en ese preciso tiempo. Esto puede describirse en VHDL usando por ejemplo wait for nnn ms;   hasta llegar al tiempo de simulación deseado y luego usar el assert respectivo. Otro modo de 'esperar' hasta un cierto valor del estimulo es usar un wait until tal como se usa en los test bench ejemplos descriptos a continuación. 
El uso de verificación de datos dentro del test bench es una técnica que tiene muchas ventajas a pesar de ser una tarea complicada la escritura del mismo. Pero la automatización de la verificación ayuda mucho en casos que se tengan diferentes test benches, y se quiera verificar el comportamiento del DUT de manera mas rápida. Este procedimiento se usa mucho en lo que se llama "Regression Tests", en los cuales se corren diferentes test benches en modo batch y la respuestas de cada uno se da a conocer por medio de los resultados de los asserts.  
Más detalle del uso de assert puedes encontrarlo en mi previo blog.

Conclusión


A modo de conclusión de este blog, a continuación se describen tres distintos test bench de un mismo componente (un decodificador 2:4). Cada TB tiene un nivel de complejidad mayor. 

Primer Caso: Test Bench simple



 1 -- Test Bench para verificar
 2 -- el comportamiento de un deco 2:4
 3 entity tb2_decode is
 4 end tb2_decode;
 5
 6 architecture test_bench of tb2_decode is
 7
 8 type input_array is array(0 to 3) of std_logic_vector(1 downto 0);
 9 constant input_vectors: input_array :=   ("00", "01", "10", "11");
10
11 signal in1  : std_logic_vector (1 downto 0);
12 signal out1 : std_logic_vector (3 downto 0);
13
14 component decode
15   port (
16     in1 : in  std_logic_vector(1 downto 0);
17     out1: out std_logic_vector(3 downto 0));
18 end component;
19 begin
20 decode_1: decode port map(
21         in1  => in1,
22         out1 => out1);
23
24 -- generacion de los estimulos
25 apply_inputs: process
26 begin
27   for j in input_vectors‘range loop
28     in1 <= input_vectors(j);
29     wait for 50 ns;
30   end loop;
31   wait;
32 end process apply_inputs;
33
34 -- verificacion de datos
35 test_outputs: process
36 begin
37   wait until (in1 = "01");
38   wait for 25 ns;
39   assert (out1 = "0110")
40     report"Output not equal to 0110"
41       severity ERROR
42   -- check the other outputs
43   wait;
44 end process test_outputs;
45 -- ...
46 end test_bench;



Segundo Caso: Test Bench Elaborado I


 1 -- Test Bench elaborado para verificar  
 2 -- el comportamiento de un deco 2:4
 3 entity tb3_decode is
 4 end tb3_decode;
 5
 6 architecture test_bench of tb3_decode is
 7
 8 type decoder_test is record
 9   in1: std_logic_vector(1 downto 0);
10   out: std_logic_vector(3 downto 0);
11 end record;
12
13 type test_array is array(natural range <>) of decoder_test;
14
15 constant test_data: test_array :=
16    ("00", "0000",
17     "01", "0010",
18     "10", "0100",
19     "11", "1000");
20
21 component decode
22   port (
23     in1 : in  std_logic_vector(1 downto 0);
24     out1: out std_logic_vector(3 downto 0));
25 end component;
26
27 begin
28 decode_1: decode port map(
29         in1  => in1,
30         out1 => out1);
31
32 apply_inputs_test_outputs: process
33 begin
34   for j in test_data’range loop
35     in1 <= test_data(j).in1);
36     wait for 50 ns;
37        assert (out1 = test_data(j).out1)
38         report "Output not equal to the expected value"
39       severity ERROR;
40   end loop;
41 end process apply_inputs_test_outputs;
42
43 end test_bench;



En este caso, el arreglo es un poco más complicado, ya que se tienen dos columnas de datos de diferente tamaño cada una. La primer columna representa el estimulo, mientras que la segunda columna representa el valor esperado como respuesta del sistema. Al describir el estímulo de esta manera de puede realizar la comparación, usando assert, dentro del mismo for-loop usado para la generación de estímulos tal como se detalla en el código ejemplo de arriba.
Otro punto a destacar de este ejemplo es el hecho que el rango de test_array no ha sido definido en su declaración. Sin embargo cuando se declara la constante test_data el rango del arreglo es definido, de este modo el analizador de VHDL no tendrá problemas en asignarle el rango a la senal test_array durante el proceso de análisis. De esta manera se facilita el cambio del tamaño de la constante sin tener que cambiar la declaración del arreglo. Del mismo modo se explica el uso de ‘range’ en el for-loop, así el for-loop se ajustara al tamaño del arreglo, sin tener que modificarlo cada vez que se cambie el tamaño del arreglo.

Tercar Caso: Test Bench sincrónico
En este caso se debe tener en cuenta el reloj, por ello para se describe un proceso un poco complicado en el cual se genera el reloj, se generan los datos estímulos y también se verifica el resultado. A mi particularmente no me gusta mucho mezclar tanto, sino mas bien tener un proceso para cada tipo de datos, y uno o más para la verificación de datos tal como se detalla mas arriba, pero a modo de ejemplo para tener más idea de lo que se puede hacer, el código que se detalla es un buen ejemplo.


 1 -- ...
 2 begin
 3 decode_1: decode port map( in1 => in1, out => out1);
 4 apply_inputs: process
 5 begin
 6   for j in test_data’range loop
 7     in1 <= test_data(j).in1);
 8     clk <= ‘0;
 9     wait for 5 ns;
10     clk <= ‘1;
11     wait for 5 ns;
12        assert (out1 = test_data(j).out1)
13         report "Output not equal to the expected value"
14       severity ERROR;
15   end loop;
16 wait;
17 end process apply_inputs;
18 -- ...

Espero q este blog sea útil ¡ ¡ ¡ Para la version .pdf click acá :)