Documentos de Académico
Documentos de Profesional
Documentos de Cultura
El presente documento pretende ser una guía básica para el uso de VHDL. Se indicarán detalles
sobre el lenguaje mediante ejemplos, tanto en sintaxis como uso de las diversas herramientas
que posee VHDL.
El objetivo de este tutorial es que sea posible diseñar códigos de manera rápida utilizando lo
esencial del lenguaje. No se dará énfasis al uso de Testbenches asociados a simulación debido a
que las herramientas más ampliamente utilizadas (Max+Plus y Xilinx ISE) permiten realizar en
forma gráfica y mucho más simple este proceso.
Se irá explicado mediante ejemplos los conceptos mínimos de programación. Se debe revisar
los comentarios a los códigos pues poseen información útil a la hora de revisar sintaxis.
Se asume que el lector posee conocimientos sobre lógica combinacional y secuencial, y trabaja ya
sea con la plataforma Altera o Xilinx, o pretende usarlas.
Qué es VHDL?
Son las siglas de ‘VHSIC Hardware Design Language’, y VHSIC es acrónimo de ‘Very High Speed
Integrated Circuits’. Desde aquí se deduce que es un lenguaje para la creación de arquitecturas de
circuitos integrados. Se ha desarrollado de manera satisfactoria la rama digital de diseño, no así la
analógica (las siglas no dicen nada sobre si es diseño analógico o digital).
Otro lenguaje conocido para esta labor es Verilog HDL: la principal diferencia con el anterior es
que Verilog HDL está basado en la estructura de C, mientras que VHDL lo hace con Pascal. Sin
embargo, la lectura de VHDL es más simple debido a su acercamiento al lenguaje inglés.
La gran ventaja de este lenguaje es la abstracción de la realización física del circuito integrado
que se desea diseñar; esto quiere decir que lo que se debe considerar es el comportamiento final
del circuito. Por ejemplo, para realizar un circuito secuencial no se requiere saber cuál será la
implementación óptima con flip-flops, sino sólo realizar la implementación de la transición entre
estados.
Comenzando con el lenguaje
Se debe considerar que en este lenguaje no se trabaja en principio con enteros o decimales, sino
con niveles lógicos, aunque no solamente 1’s y 0’s. También existen otros como ‘Z’ (alta
impedancia), ‘X’ (don’t care) y ‘U’ (indefinido). Estos y otros estados pertenecen al tipo
STD_LOGIC, que es el estándar para trabajar con variables lógicas en VHDL.
Este lenguaje además no es sensible a mayúsculas (case sensitive), lo que permite que por
ejemplo MI_VARIABLE y My_VArIable sean el mismo elemento. A pesar de esto, los nombres no
deben comenzar con números, ni tener caracteres extraños (acentos, por ejemplo), ni doble guión
bajo (__).
Previamente, para evitar confusiones, se dará una pequeña reseña sobre la estructura de los
bloques en VHDL.
Función (parámetros)
Declaración de variables
Begin
Instrucciones
End función;
Esta estructura (con algunas modificaciones de acuerdo a cada situación) es heredada a VHDL.
library IEEE;
use IEEE.STD_LOGIC_1164.all; -- librerias necesarias para usar std_logic
-- Salida
OUT_1 : out STD_LOGIC -- El ultimo puerto no lleva ‘;’
);
end CIRCUITO;
architecture S of CIRCUITO is
-- definición de una señal
-- SIGNAL NOMBRE_SEÑAL : TIPO SEÑAL
signal SALIDA_AND : STD_LOGIC -- las señales (SIGNAL) se pueden considerar
-- como un ‘alambre’
begin
SALIDA_AND <= IN_1 and IN_2;
OUT_1 <= SALIDA_AND or IN_3;
end S;
Hay que tener en cuenta que todo lo que está dentro de begin se realiza en paralelo. Es decir, se
podría haber escrito:
begin
OUT_1 <= SALIDA_AND or IN_3;
SALIDA_AND <= IN_1 and IN_2;
end S;
y el resultado sería el mismo. Como las acciones dentro de una architecture se realizan en
paralelo, no se requiere verificar si las entradas han cambiado para actualizar los datos (en este
caso OUT_1), pues la arquitectura se puede considerar como el conjunto de conexiones físicas del
circuito. Junto con esto, las variables definidas como signal son bidireccionales, no así las
variables definidas en PORT (excepto las definidas como inout, de las que se habla más adelante).
En programas grandes se debe tener especial atención en la asignación doble y las asignaciones
circulares:
La ecuación que rige a este dispositivo es: Qk +1 = Qk J +Qk K , controlada mediante una señal de
reloj.
El código es el siguiente:
library IEEE;
use IEEE.STD_LOGIC_1164.all;
entity FF-JK is
port (
J : in STD_LOGIC;
K : in STD_LOGIC;
CLOCK : in STD_LOGIC;
Q : out STD_LOGIC;
NOT_Q : out STD_LOGIC;
);
end FF-JK;
architecture A of FF-JK is
signal Q_ANT, Q_NOW: STD_LOGIC; -- así se pueden definir dos o más variables del mismo tipo
begin
process (CLOCK)
-- entra a este bloque SOLO cuando ocurre un cambio en CLOCK.
-- Para indicar más de una entrada se escribe PROCESS (IN_1, IN_2, ... IN_N)
begin
if (CLOCK’EVENT and CLOCK=1) then -- condicional
-- clock’event indica si ha existido cambio en clock
-- (puede ser cualquier variable).
end process;
Q <= Q_NOW;
NOT_Q <= NOT(Q_NOW);
end A;
Mientras no ocurra un cambio en el clock, las señales Q_ANT y Q_NOW no se modificarán, y por lo
tanto Q y NOT_Q permanecerán invariantes a pesar que J y K cambien, pues sólo cuando se
ejecuta el bloque ‘PROCESS (CLOCK)’ las señales cambian.
En este caso no es necesario revisar si se cumple CLOCK’EVENT, pues sólo CLOCK activa el proceso,
pero si hubiese más de un elemento de activación del proceso sería necesario revisarlo.
Es recomendable realizar fuera de los PROCESS todas las asignaciones posibles para una más rápida
implementación (por ejemplo, variables no requieran una asignación secuencial). Así también para
una mejor comprensión del código conviene no asignar los puertos de salida dentro de los
PROCESS, sino trabajar con señales y luego asignar las señales a las salidas. Esto además oculta las
transiciones intermedias de niveles lógicos (carreras) hacia la salida.
En términos generales se puede considerar que un bloque de PROCESS es una instrucción de la
arquitectura, por lo tanto, si existe más de un PROCESS, todos estos se ejecutarán en paralelo. Esto
se debe tener en cuenta en caso que más de un PROCESS sea activado por la misma entrada, pues
pueden modificar a las mismas señales al mismo tiempo.
Otros condicionales
Otras instrucciones como el ‘if’ para realizar condicionales son las siguientes:
IF (Uso en process)
If (condicion) then
Instrucciones secuenciales
Elsif (condicion2) then
Instrucciones secuenciales
Else
Instrucciones secuenciales
End if;
case EXPRESION is
when VALOR_1 =>
-- instrucciones secuenciales
Siempre se debe poner un valor para when others en un with – select pues valores válidos son
también por ejemplo alta impedancia y don’t care.
Uso de arreglos (vectores)
Elementos importantes son los vectores, pues se utilizan principalmente como buses de datos o
para manejar señales de más de un bit (como por ejemplo la dirección de una memoria ROM).
Para el uso de cadenas de bits (por ejemplo un nibble o byte) se utiliza preferentemente la
primera forma, quedando el bit más significativo en la posición mayor. Si se asigna:
En el siguiente ejemplo se implementará una memoria ROM básica de 4x8, mostrando el uso de
los vectores:
library IEEE;
use IEEE.STD_LOGIC_1164.all;
entity ROM is
port ( ROM_ADDR : in STD_LOGIC_VECTOR (1 downto 0);
ROM_DATA : out STD_LOGIC_VECTOR (7 downto 0);
ROM_OE : in STD_LOGIC) -- Output enable. Si está deshabilitada
-- cambia a alta impedancia
end ROM;
architecture A of ROM is
begin
process (ROM_ADDR, ROM_OE) -- en process se puede indicar más
begin -- de una variable que cambie
if (ROM_OE=1) then
with ROM_ADDR select
ROM_DATA <= "00000000" when "00",
ROM_DATA <= "00000001" when "01",
ROM_DATA <= "00000011" when "10",
ROM_DATA <= "00000111" when OTHERS;
-- se debe poner others debido a las otras posibles entradas
-- (alta impedancia, don’t care, etc)
else
ROM_DATA <= "ZZZZZZZZ"; -- alta impedancia
end if;
end process;
end A;
Si por ejemplo el vector es muy largo, no es necesario escribir toda la cadena. Existen dos
maneras muy útiles de asignar valores a vectores:
Hay otras formas, pero generalmente se trabaja con arreglos de tamaño potencias de 2, así que
con la asignación en hexadecimal es suficiente. La asignación hexadecimal señalada aquí sólo es
válida para la versión 93 de VHDL.
Concatenación
VECTOR3 <= VECTOR1 & VECTOR2; -- deben coincidir los tamaños finales
Uso de componentes
Supongamos que queremos realizar un flip-flop T a partir de uno D. Las ecuaciones son:
T_DATA_IN = D_CLOCK;
D_DATA_IN = not D_Q;
Supongamos que ya teníamos una entidad llamada FF_D que es un flip-flop D, con entradas
DATA_IN y CLOCK, y salidas Q y NOT_Q, y se desea usar para realizar el Flip-flop T.
library IEEE;
use IEEE.STD_LOGIC_1164.all;
entity FF_T is
port (
DATA_IN : in STD_LOGIC;
Q : out STD_LOGIC;
NOT_Q : out STD_LOGIC
);
end FF_T;
architecture A of FF_T is
component FF_D -- Aquí se indica que se usará la entidad FF_D
port (
DATA_IN : in STD_LOGIC; -- Se indica su estructura.
CLOCK : in STD_LOGIC; -- Los nombres de los puertos
Q : out STD_LOGIC; -- deben coincidir con los originales
NOT_Q : out STD_LOGIC );
end component;
begin
FD : FF_D port map (
-- puerto de componente => puerto o señal de entidad
DATA_IN => CXN_D_Q, -- se separan por ‘,’
CLOCK => DATA_IN,
Q => CXN_D_Q, -- se conecta Q con DATA_IN
NOT_Q => NOT_Q -- el ultimo no lleva ‘,’
);
end A;
Se creó la señal CXN_D_Q pues no se pueden conectar dos puertos de componentes entre sí
directamente, ni tampoco dos puertos del mismo componente. Una entidad se puede entender
como un circuito, y un componente como un integrado. Para conectar por ejemplo dos pines del
integrado entre ellos, es necesario un cable (signal) que lo realice. Un puerto también puede
conectarse directamente a una salida del circuito (idealmente) sin necesidad de ningún cable.
Hay que tener en cuenta que para la correcta compilación de los componentes, el nombre de
archivo debe coincidir con el de la entidad. En Xilinx ISE se debe tener atención que los nombres
de los componentes sean además coincidentes en mayúsculas/minúsculas.
Si se desea realizar un circuito con dos o más componentes del mismo tipo, se deben crear los
port map necesarios. La definición de component es sólo una:
architecture A of VARIOS_FF is
component FF_D
port (
... );
end component;
...
begin
...
FD_A : FF_D port map (
... ) ;
Enumeración
La estructura es la siguiente:
Por ejemplo, se desea crear un circuito secuencial que entregue un 1 si han ingresado dos unos
sucesivos. La tabla de transición de estados sería:
entity MAQ_ESTADO is
port (
DATA_IN : in STD_LOGIC;
OUT : out STD_LOGIC;
CLOCK : in STD_LOGIC
);
end MAQ_ESTADO;
architecture A of MAQ_ESTADO is
En el proceso de compilación VHDL asigna el tamaño y valores para implementar todos los
estados necesarios.
Estructuras
Las estructuras se dividen en dos conjuntos: arreglos (array) y registros (record). La principal
diferencia es que los arreglos permiten señales de un solo tipo, no así los registros.
Por ejemplo, para la implementación de una matriz de std_logic de 16x8 se puede crear un type
std_8bits y luego crear un arreglo de std_8bits:
Ahora, si se desea realizar un registro, por ejemplo, para describir un píxel en RGB, el código
sería:
architecture A of EJEMPLO is
type PIXEL is record
R : STD_LOGIC_VECTOR (7 downto 0); -- separados por ‘;’
G : STD_LOGIC_VECTOR (7 downto 0);
B : STD_LOGIC_VECTOR (7 downto 0); -- no necesariamente tienen que ser
end record; -- del mismo tipo
Hay que considerar que para realizar cualquier asignación las variables deben ser del mismo tipo y
tamaño.
Operaciones matemáticas
VHDL posee librerías que poseen herramientas matemáticas como sumas, conversión, resta, etc.
Para la librería STD_LOGIC_ARITH el tipo de dato más usado es STD_INTEGER.
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.STD_LOGIC_ARITH.all;
entity RAM is
port ( RAM_ADDR : in STD_LOGIC_VECTOR (1 downto 0);
RAM_DATA_IN : out STD_LOGIC_VECTOR (7 downto 0);
RAM_DATA_OUT : out STD_LOGIC_VECTOR (7 downto 0);
RAM_CE : in STD_LOGIC; -- chip enable
RAM_WE : in STD_LOGIC); -- write enable
end RAM;
architecture A of RAM is
type MEM_RAM is array(3 downto 0) of STD_LOGIC_VECTOR (7 downto 0);
signal MEMORIA :MEM_RAM;
signal RAM_DATA_SIGNAL : STD_LOGIC_VECTOR (7 downto 0);
begin
process (RAM_ADDR, RAM_OE, RAM_WE)
begin
if (RAM_CE=1) then
if (RAM_WE=1) then
-- se escribe en la RAM
MEM_RAM(conv_integer(RAM_ADDR)) <= RAM_DATA_IN;
end if;
end if;
end process;
end A;
La tarjeta de desarrollo Xilinx posee una memoria RAM, por lo que es siempre conveniente realizar
la implementación de RAM’s y ROM’s en esta zona para no utilizar los recursos de la FPGA. Para
revisar cómo realizarlo (y ver implementaciones más específicas) se puede revisar la dirección:
http://toolbox.xilinx.com/docsan/xilinx6/books/data/docs/xst/xst0026_5.html. Además, la
plataforma UP2 de Altera mediante Max+Plus II permite trabajar con librerías internas llamadas
LPM, que están documentadas en la ayuda de Max+Plus II.
LIBRARY ieee;
USE ieee.std_logic_1164.all;
USE ieee.std_logic_unsigned.all;
ENTITY div_clock IS
port (
in_clock : IN STD_LOGIC;
out_clock : OUT STD_LOGIC
);
END div_clock;
Los puertos bidireccionales no pueden trabajar como entrada y salida al mismo tiempo, sino que
debe alternarse su estado. Esto que parece simple no lo es, pues debe realizarse la asignación
alámbrica de entrada y salida, es decir, fuera de un process. Esto puede generar muchos
problemas de doble asignación, al imponerse involuntariamente dos valores como salida, o un
valor de entrada y otro de salida simultáneamente.
Para recibir información por el puerto, la salida debe asignarse como alta impedancia, pues este
estado permite que desde el otro terminal del puerto se impongan valores lógicos.
El puerto P0 del 8031 se utiliza para la adquisición de las instrucciones del programa a ejecutar:
en un instante envía por P0 y parte de P2, la dirección de memoria a leer de la ROM, y al
siguiente espera a que se envíe la instrucción. Para determinar si el puerto trabaja como entrada
o salida se utilizan las señales del microcontrolador ALE (address latch enable) y PSEN (program
strobe enable). El modo se determina el estado se detalla a continuación:
1
PSEN 0
1
ALE 0
Cuando ALE es uno, P0 impone el valor de la dirección de memoria a recibir. Cuando ALE es cero,
se mantiene el último valor de P0 en un registro de latch, lo que permite que P0 cambie a modo
de entrada.
Sin embargo, mientras PSEN no sea cero, la ROM no enviará la instrucción, y permanecerá en alta
impedancia. Cuando PSEN es cero, la ROM envía la instrucción a P0, que ya está en modo de
entrada de datos. Esto se realiza para que no existan inconsistencias de valores lógicos en las
transiciones.
Se desea simular la ROM y el latch (ambos asíncronos) para comunicarse con un 8031. La
descripción de los puertos y señales es la siguiente:
Microcontrolador:
uc_PSEN : PSEN
uc_ALE : ALE
uc_P0 (7 downto 0) : P0
ROM:
rom_addr (7 downto 0) : dirección de memoria a leer
rom_data (7 downto 0) : instrucción de la ROM
Señales:
addr_latch_signal (7 downto 0): recibe la dirección de memoria desde P0
data_signal (7 downto 0) : almacena la instrucción de la ROM para enviarse a P0
Hay que considerar que por simplicidad la dirección de memoria a utilizar será sólo de 8 bits, aunque el 8031
permite direcciones de hasta 14 bits (los otros bits se envían por P2). Además, la definición es vista desde la
ROM: P0 trabaja en modo de salida cuando la ROM le envía datos, pues salen hacia el microcontrolador; y es
entrada cuando se recibe la dirección de memoria en la ROM.
-- Conexión bidireccional de P0
-- uc_P0 como salida. Alta impedancia es para permitir entrada de información
-- sólo se envía la instrucción cuando uc_PSEN es cero
uc_p0 <=data_signal when (uc_PSEN='0') else (others =>'Z');
Por simplicidad, en este ejemplo no se consideraron casos de falla (por ejemplo que PSEN sea
cero antes que ALE). Esto se puede considerar en las condiciones de entrada y salida de uc_P0, y
definir un estado intermedio, muy posiblemente asignando alta impedancia a uc_P0.