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

jueves, 5 de abril de 2012

Modelacion Funcional de Bus (Bus Functional Model)


Introducción

BFM es una sigla muy usada en el ámbito de diseño de HDL-FPGA, sin embargo no todos saben que es ni a que se refiere, otros diseñadores se dedican a escribir BFMs por lo que saben muy bien lo que es. Bueno, este blog es para los que pertenecen al grupo que dice “Qué es eso?” cuando escucha ‘BFM’.

Básicamente como su nombre lo indica, BFM es un modelo de simulación simplificado que describe correctamente la funcionalidad de I/O bus de un sistema normalmente complejo, sin inmiscuirse con el comportamiento interno de ese componente.

Este modelo se usa dentro del test bench como estimulo (stimulus) del sistema que se quiere testear (DUT), permitiendo en forma directa una comunicación entre el DUT y el sistema externo con el que se quiere interactuar, evitando de esta manera tener que escribir un muy complejo test bench. La siguiente figura muestra el sistema de simulación cuando se usa un BFM.


Figura 1 – Esquema de simulación usando BFM

Explico ahora los distintos componentes en la Figura 1:
Test Code: es el código del test bench en sí, escrito en VHDL. En este parte del test bench se escribe el código VHDL para estimular el DUT, principalmente haciendo uso de los procedimientos que modelan algún ciclo de I/O del componente BFM instanciado en el test bench. 
BFM: es el código BFM del sistema que interactúa con el DUT. Este código es provisto por el fabricante del sistema/memoria o escrito por alguno de nosotros. Dentro del test bench se crea una declaración de este componente y su respectiva instanciación. Actúa como generación de estimulo para el DUT al proveer un preciso estimulo del protocolo de comunicación, ciclo por ciclo en funcionalidad y tiempo. Al mismo tiempo el BFM acepta comandos para seleccionar la funcionalidad a ser modelada. Estos comandos son normalmente asignados a alguna señal en el test bench, declarada en el  paquete, y ejecutados por el procedimiento respectivo del BFM. Este bloque básicamente se codifica como una simple maquina de estados, cuyos estados por ejemplo pueden ser read, write, idle. Y de acuerdo a lo que el test code ‘solicita’ la máquina de estados pasa al respectivo estado y activa las I/O necesarias del sistema modelado (BFM).
DUT: es el device under test que queremos depurar.
Package: es la unidad de diseño que contiene todas las declaraciones y descripciones de las señales, funciones, procedimientos relacionadas al test bench y al BFM. Actúa como medio de comunicación entre las señales del test bench y los procedimientos/funciones del BFM.
El símbolo * lo uso para detallar que el código de test y el BFM ‘pueden’ (no es siempre el caso) proveer no solo el estimulo al DUT, sino también chequear la respuesta del DUT a cierto estimulo. Esta funcionalidad es opcional, sin embargo es altamente aconsejada para automatizar el test y proveer un rápido resultado del comportamiento del DUT bajo determinado estimulo.

Quién escribe un BFM?

Normalmente para componentes complejos, por ejemplo memorias DDR, Flash, dispositivos PCI, PCIe, etc; el fabricante ofrece un modelo BFM para que el diseñador pueda verificar que el protocolo que ha diseñado interactúa correctamente con el componente con el que quiere comunicarse.
Ahora, bien el BFM, es en sí un modelo escrito en VHDL por un ingeniero de la empresa fabricante, por lo que el modela será tan bueno como el ingeniero que lo escriba.
Debo aclarar que hay algunas empresas que escriben sus propios BFMs a fin de facilitar la simulación de componentes, normalmente varios, que tienen protocolos de comunicación complejos. En algunas de estas empresas tuve la oportunidad de escribir y usar algunos BFModels, tarea que al principio era linda, pero luego se transformó en tediosa… :) 

Porqué usar BFM?

El proceso de prueba (simulación) a veces es una tarea compleja que involucra muchas horas de escritura del código (test bench) y otras más de correr la simulación. Normalmente más de la mitad del tiempo de un proyecto involucra la simulación del mismo, y más se incrementa este tiempo cuando la complejidad del proyecto crece. Por ello es necesario usar métodos de prueba (test) más eficientes y que sigan asegurando un diseño libre de errores.
El uso de BFM habilita el test de complejos protocolos y funciones con un gran nivel de abstracción de la funcionalidad interna del componente simulado, menos instrucciones a simular, preciso timing de la interface, todo lo que resulta en un test más robusto, completo y sobre todo más rápido de ejecutar. Esta última es una característica que se destaca cuando se comparan los tiempos de simulación de sistemas complejos, IP cores, etc., que usualmente toman un largo tiempo en simular.

Características de un BFM

  • Provee un medio para modelar la interface del componente que se está modelando, SIN importar la estructura interna del componente o si el componente en si tiene un microprocesador, una o varias FSMs, etc.
  • Provee información de todos los tiempos del protocolo de comunicación de I/Os.
  • Provee chequeo funcional para verificar el correcto funcionamiento del protocolo (handshaking).
  • Normalmente son escritas usando las instrucciones típicamente usadas en Test Bench. Paquetes, funciones, procedimientos, registros y tipos son comúnmente encontradas en BFMs.
  • El alto nivel de abstracción que VHDL ofrece permite modelar complejos sistemas, en funcionalidad y tiempo, abstrayendo al diseñador de la complejidad interna del sistema modelado. 

Ejemplo Flujo de Uso de BFM

La siguiente figura detalla el procedimiento que se debería seguir cuando se usa un BFM.

 
Figura 2 - Pasos a seguir para usar los procedimientos del BFM

Paso 1: Dependiendo de la funcionalidad del BFM que se desea invocar, en el código del test bench se genera un llamado al procedimiento respectivo asignando los valores necesarios. Por ejemplo para comenzar un ciclo de escritura (Wr), habrá que asignar a la senal respectiva un ‘valor’ que entienda el procedimiento que se desea invocar.
Paso 2: El procedimiento invocado seguramente necesita generar algún dato o señal antes de invocar al procedimiento principal.
Paso 3: El procedimiento principal primero solicita la disponibilidad del bus de I/O, normalmente por medio de alguna señal de pedido (request) de bus. En caso que el bus no este disponible, por ejemplo porque todavía se está ejecutando algún otro procedimiento, deberá esperar hasta que el bus esté disponible.
Paso 4: Las I/O del BFM son controladas por el procedimiento principal con el comportamiento deseado (Wr/Rd, etc.) y con el respectivo timing. Una vez finalizado el ciclo, el procedimiento avisa por medio de la misma senal de ocupado (busy)  desacertándola. Permitiendo así el recomienzo del ciclo.

Escritura de BFMs

Uno de los principales usos de BFMs es por ejemplo para representar ciclos de Wr/Rd del bus del componente que se desea modelar. Estos ciclos normalmente se describen mediante el uso de funciones y procedimientos definidos en un paquete para permitir la accesibilidad no solo del BFM pero también del test bench y del código VHDL  en sí.
Los ciclos de Wr/Rd son fácilmente descriptos mediante el uso de procedimientos (procedures) que modelan perfectamente el comportamiento de los ciclos de Wr/Rd ya sea sobre memoria o sobre I/O, no solo funcionalmente sino también con los tiempos respectivos. La BFM debe ser capaz de leer un comando, por ejemplo, y devolver la respuesta a ese comando, de ese modo se prueba tanto la escritura como la lectura de mi componente sobre el componente modelado con BFM. Al usar procedimientos y ser un modelo de un sistema toda la potencia del lenguaje VHDL puede ser usada al escribir el BFM.
La entidad/arquitectura del BFM debe ser instanciada junto con nuestro componente (DUT) dentro del test bench. Mínima configuración del BFM a veces es necesaria al comienzo del TB, hasta que pueda comenzar el handshaking entre mi componente y el componente BFM.
Ejemplo del código escrito para una BFM.
Declaraciones en el paquete:

 1 -- declaraciones de type y señales --    
 2 type cycle_op is (idle, nop, read, write);
 3
 4 type instruction_op is record
 5    bus_cycle   : cycle_op;
 6    addr_bus    : std_logic_vector(15 downto 0);
 7    data_bus    : std_logic_vector(15 downto 0);
 8    rd_data     : std_logic_vector(15 downto 0);
 9    bus_request : std_logic;
10    busy        : std_logic;
11 end record;
12
13 signal cmd     : instruction_op :=  (idle,
14                                     (others => '0'),
15                                     (others => '0'),
16                                     (others => 'Z'),
17                                     '0',
18                                     'Z');
 
En este ejemplo el registro (record) declara las señales a ser usadas para generar el estímulo de las I/O, como así también información a pasar al procedimiento que debe ejecutar el ciclo seleccionado. Es decir se declara:
  • Tipo del ciclo (idle, nop, read, write)
  • Dirección a ser utilizada en el bus de direcciones
  • Dato a ser escrito si es un ciclo de escritura, o dato que se espera leer si es un ciclo de lectura
  • Data leído
  • Señal de pedido de bus (bus_request). Se activa para solicitar comienzo de un ciclo.
  • Señal de bus ocupado (busy). Se activa durante el proceso de ejecución de algún ciclo. 
Un procedimiento de un ciclo de escritura puede ser escrito tal como se detalla a continuación:

1 -- declaracion del procedimiento wr_cycle
2 procedure wr_cycle (
3    constant p_addr_bus : in std_logic_vector(15 downto 0);
4    constant p_data_bus : in std_logic_vector(15 downto 0);
5    signal   p_cmd_op   : inout instruction_op
6    ) is
7 begin
8     main_cycle(write,p_addr_bus, p_data_bus,p_cmd_op);
9 end wr_cycle;

 

Donde el procedimiento main_cycle es el siguiente:

 1 -- declaracion procedimiento main_cycle
 2 procedure main_cycle(
 3    constant p_cycle    : in cycle_op;
 4    constant p_addr_bus : in std_logic_vector(15 downto 0);
 5    constant p_data_bus : in std_logic_vector(15 downto 0);
 6    signal   p_cmd_op   : inout instruction_op
 7    ) is
 8 begin
 9   p_cmd_op.request <= '0';
10   if p_cmd_op.busy /= '0' then
11       wait on p_cmd_op.busy until p_cmd_op.busy = '0';
12   end if;
13   p_cmd_op.request   <= '1';
14   p_cmd_op.bus_cycle <= p_cycle;
15   p_cmd_op.addr      <= p_addr;
16   p_cmd_op.data      <= p_data;
17   if (p_cmd_op.busy = '0') then
18      wait on p_cmd_op.busy until p_cmd_op.busy = '1';
19   end if;
20   p_cmd_op.request    <= '0';
21 end;

Este procedimiento lleva a cabo las tareas de ‘handshaking’, usando las señales busy y request, y de comunicación entre el test code y el BFM.
Ahora, como invoco las distintas funciones del BFM dentro del test bench? La siguiente línea de código, escrita en el test bench, muestra como se puede invocar el procedimiento del ciclo de escritura:  

1 wr_cycle(x"0000",x"05ab",cmd_op);

Entonces lo que pasa es lo siguiente:
  • El procedimiento wr_cycle esta declarado en un paquete (ver figura 1)
  • El procedimiento wr_cycle le pasa los parámetros necesarios al procedimiento main_cycle
  • El procedimiento main_cycle lleva a cabo el handshaking, y pasa el comando (cmd_op) a la máquina de estados (FSM, Finite State Machine) de la BFM.
  • Al ser un comando de escritura la máquina de estado pasa al estado escritura donde procederá a activar las señales correspondientes del ciclo de escritura, y habilitar las de espera que el proceso se cumpla (ready). Este último paso es el que realmente ‘ve’ el DUT.
La siguiente figura detalla la modificación de la primer figura al ejecutarse un ciclo de escritura, mostrando gráficamente los pasos recién enumerados.


Figura 3 - Detalle de los procedimientos y código ejecutados durante un ciclo de escritura del BFM

Unos puntos a considerar de la Figura 3:
  • La línea punteada entre el bloque Test Code y Package se debe a que en realidad el procedimiento wr_cycle está definido en el paquete pero, como bien sabemos, cuando se compila el código de Test Code, el procedimiento definido en el paquete es ‘insertado’ en el lugar en que éste es invocado
  • Lo mismo sucede con el procedimiento main_cycle. El cual es también ejecutado desde Test Code
  • Tal como se puede deducir del código de los procedimientos detallados anteriormente (wr_cycle, main_cycle), la señal cmd_op, definida como un registro, contiene toda la información necesaria para que la FSM del componente BFM genere el correspondiente ciclo de escritura en las líneas de I/O de este bloque (recordar que el componente BFM está instanciado dentro del test bench).

Espero que este artículo sirva para, por lo menos, tener una idea de la pregunta: Qué es un BFM?

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 !

domingo, 8 de mayo de 2011

Repaso de 'functions' - Valor Inicial Sintetizable? ? ? ? , Siii .... pero solo en "functions"

Funciones (functions) son una de las formas de subprogramas ofrecidas por VHDL, la otra forma son los procedimientos (procedures). Una función puede ser llamada desde una expresión. El lugar mas común para una expresión en VHDL es sobre el lado derecho (RHS, right-hand side) de una instrucción de asignación o en la condición de una instrucción if o case.
La principal, pero no única, aplicación de funciones es ejecutar operaciones que se repiten en el código VHDL. Similar a las funciones usadas en otros lenguajes como 'C'. Sin embargo debido a ésta similitud diseñadores con conocimientos de lenguajes de programación cometen el error de abusar del uso de las funciones como forma natural de jerarquía. Por el contrario, la forma natural de jerarquía en VHDL es el componente compuesto de entidad y arquitectura.
Algunos puntos importantes para refrescar con respecto a las funciones:
- Las funciones retornan un único valor que será asignado a un dato objeto. Funciones NO pueden ser llamadas por sí solas.
- Pueden ser invocadas como expresión de una instrucción concurrente o como una expresión de una
instrucción secuencial (dentro de un proceso), aunque la función en sí SOLO puede tener instrucciones secuenciales.
- Variable declaradas dentro de la función NO retienen el valor entre ejecuciones. Ellas son re-inicializadas cada vez que la función es llamada. Totalmente distinto al comportamiento de las variables en los procesos.
La sintaxis general de una función es la siguiente:

function function_name(lista_parametros)return tipo_retorno is
  declaraciones; -- se aconseja variables o constantes únicamente
begin
  instrucciones_secuenciales;
  return simple_valor;
end [function] [function_name];

Donde:
- "lista_parametros" se refiere a los parámetros que se asociarán a los parámetros con que se invoca a la función. La asociación de los parámetros, cuando se invoca la función, puede ser por posición o por nombre. El único modo permitido para los objetos definidos en la lista de parámetros es el modo 'in', que por defecto se asigna. La única clase permitida es clase signal. Importante: declare los parámetros sin un determinado tamaño, es decir genéricos. De este modo la misma función puede ser invocada con distintos tamaños de vectores por ejemplo.
- 'declaraciones' se refiere a la parte declarativa de la función. Las únicas clases permitidas para ser declaradas son clase variable y constante. Importante: Variables pueden ser declaradas y usadas en las funciones para acumular resultados o mantener valores intermedios. Pero, las variables dentro de la función NO mantienen el valor entre llamados a la función.
- 'instrucciones_secuenciales' se refiere a la parte de la función donde se escribirán las respectivas instrucciones secuenciales que hacen a la función.
- 'return_simple_valor' se refiere a que toda función debe tener por lo menos un return. Pueden haber varios return pero solo uno se ejecutará. Puede que return sea una expresión compleja que calcula directamente el valor a retornar o sea una simple variable que tiene el valor a retornar.
- 'tipo_retorno' se refiere al tipo (type) del dato objeto de retorno. Este valor sera asignado al objeto del lado izquierdo de la instrucción que invoca a la función. Por lo que ambos tipos deben coincidir.

Muuuy Importante: los valores iniciales asignados a las variables durante su declaración SON sintetizables !
Una función puede ser declarada en:
- La parte declarativa de la arquitectura del componente. Esto se aconseja hacerlo cuando la función se usará  solo en ese componente.
- La parte declarativa de un proceso. Rara vez usado.
- Puede ser declarada dentro de otra function o procedure.
- En un paquete, En este caso la función queda disponible para todo los componentes del proyecto. Para poder acceder a la función desde cualquier componente recuerde usar:

use work.nombre_del_paquete.all;

Una de las características mas útil de las funciones es la posibilidad de definir los parámetros de entradas como un arreglo sin restricciones, por lo que una función puede ser vista como una familia de funciones, una para cada posible tamaño del/de los parámetros de entrada. Pero para que funciones correctamente se debe tener mucho cuidado en escribir el código de la función en forma genérica sobre todo usando atributos en el dato objeto de entrada para obtener por ejemplo tamaño, rango, etc.
Ejemplo de una función:

library ieee;
use ieee.std_logic_1164.all;

package my_pack is 
 function vector_matches(va,vb: std_logic_vectorreturn natural;
end package;

package body my_pack is 
 function vector_matches(va,vb:std_logic_vector)return natural is 
   variable va_tmp:std_logic_vector(va'length-1 downto 0):=va;
   variable vb_tmp:std_logic_vector(vb'length-1 downto 0):=vb;
 begin
   assert (va_tmp'length = vb_tmp'length)
     report ”the vectors have different size”


        severity failure;
   for i in va_tmp'range loop
       if va_tmp(i) = vb_tmp(i) then
          cnt := cnt + 1;
       end if;
   end loop;
   return cnt;
 end function;
end package body;


Básicamente el archivo conteniendo el paquete my_pack (por ejemplo my_pack.vhd) debe ser importado junto con los otros componentes .vhd en el proyecto que se esté ejecutando. La herramienta de síntesis al encontrar 'use work.my_pack.all' importará la(s) función(es) contenidas en el paquete y las reemplazará en el código de acuerdo a su uso. Ejemplo de uso de la función declarada en el paquete my_pack:

library ieee
use ieee.std_logic_1164.all; 
use work.my_pack.all;
...
entity example is
        port(...);
end;

architecture beh of example is
begin
     ...
     q_d1d2 <= vector_matches(d1,d2);-- concurrent call
                                     -- parametros por posicion
 ftn_ex_proc:process(d3,d4)
 begin
     q_d3d4 <= vector_matches(vb=>d3, va=>d4);-- sequential call
                                         -- parametros por nombre
 end process ftn_ex_proc;
. . . 
end beh;