vhdl Tutorial
Empezando con vhdl
Buscar..
Observaciones
VHDL es un acrónimo compuesto de VHSIC (circuito integrado de muy alta velocidad) HDL (lenguaje de descripción de hardware). Como lenguaje de descripción de hardware, se utiliza principalmente para describir o modelar circuitos. VHDL es un lenguaje ideal para describir circuitos, ya que ofrece construcciones de lenguaje que describen fácilmente el comportamiento concurrente y secuencial junto con un modelo de ejecución que elimina la ambigüedad introducida al modelar el comportamiento concurrente.
VHDL se interpreta normalmente en dos contextos diferentes: para simulación y para síntesis. Cuando se interpreta para la síntesis, el código se convierte (sintetiza) a los elementos de hardware equivalentes que se modelan. Normalmente, solo un subconjunto de VHDL está disponible para su uso durante la síntesis, y las construcciones de lenguaje compatibles no están estandarizadas; Es una función del motor de síntesis utilizado y del dispositivo de hardware de destino. Cuando se interpreta VHDL para simulación, todas las construcciones de lenguaje están disponibles para modelar el comportamiento del hardware.
Versiones
Versión | Fecha de lanzamiento |
---|---|
IEEE 1076-1987 | 1988-03-31 |
IEEE 1076-1993 | 1994-06-06 |
IEEE 1076-2000 | 2000-01-30 |
IEEE 1076-2002 | 2002-05-17 |
IEEE 1076c-2007 | 2007-09-05 |
IEEE 1076-2008 | 2009-01-26 |
Instalación o configuración
Un programa VHDL puede ser simulado o sintetizado. La simulación es lo que más se parece a la ejecución en otros lenguajes de programación. La síntesis convierte un programa VHDL en una red de puertas lógicas. Muchas herramientas de simulación y síntesis de VHDL forman parte de las suites comerciales de Electronic Design Automation (EDA). Con frecuencia también manejan otros lenguajes de descripción de hardware (HDL), como Verilog, SystemVerilog o SystemC. Existen algunas aplicaciones gratuitas y de código abierto.
Simulación VHDL
GHDL es probablemente el simulador VHDL libre y de código abierto más maduro. Se presenta en tres sabores diferentes según el backend utilizado: gcc
, llvm
o mcode
. Los siguientes ejemplos muestran cómo usar GHDL (versión mcode
) y Modelsim, el simulador comercial HDL de Mentor Graphics, bajo un sistema operativo GNU / Linux. Las cosas serían muy similares con otras herramientas y otros sistemas operativos.
Hola Mundo
Crea un archivo hello_world.vhd
contenga:
-- File hello_world.vhd
entity hello_world is
end entity hello_world;
architecture arc of hello_world is
begin
assert false report "Hello world!" severity note;
end architecture arc;
Una unidad de compilación VHDL es un programa completo de VHDL que se puede compilar solo. Las entidades son unidades de compilación VHDL que se utilizan para describir la interfaz externa de un circuito digital, es decir, sus puertos de entrada y salida. En nuestro ejemplo, la entity
se llama hello_world
y está vacía. El circuito que estamos modelando es una caja negra, no tiene entradas ni salidas. Las arquitecturas son otro tipo de unidad compiladora. Siempre están asociados a una entity
y se utilizan para describir el comportamiento del circuito digital. Una entidad puede tener una o más arquitecturas para describir el comportamiento de la entidad. En nuestro ejemplo, la entidad está asociada solo a una arquitectura denominada arc
que contiene solo una declaración VHDL:
assert false report "Hello world!" severity note;
La declaración se ejecutará al comienzo de la simulación e imprimirá el Hello world!
Mensaje en la salida estándar. La simulación terminará porque no hay nada más que hacer. El archivo fuente VHDL que escribimos contiene dos unidades de compilación. Podríamos haberlos separado en dos archivos diferentes, pero no podríamos haberlos dividido en archivos diferentes: una unidad de compilación debe estar completamente contenida en un archivo fuente. Tenga en cuenta que esta arquitectura no se puede sintetizar porque no describe una función que se pueda traducir directamente a puertas lógicas.
Analiza y ejecuta el programa con GHDL:
$ mkdir gh_work
$ ghdl -a --workdir=gh_work hello_world.vhd
$ ghdl -r --workdir=gh_work hello_world
hello_world.vhd:6:8:@0ms:(assertion note): Hello world!
El directorio gh_work
es donde GHDL almacena los archivos que genera. Esto es lo que dice la opción --workdir=gh_work
. La fase de análisis verifica la corrección de la sintaxis y produce un archivo de texto que describe las unidades de compilación encontradas en el archivo fuente. La fase de ejecución en realidad compila, vincula y ejecuta el programa. Tenga en cuenta que, en el mcode
versión de GHDL, no se generan archivos binarios. El programa se recompila cada vez que lo simulamos. Las versiones gcc
o llvm
comportan de manera diferente. Tenga en cuenta también que ghdl -r
no toma el nombre de un archivo fuente VHDL, como lo hace ghdl -a
, sino el nombre de una unidad de compilación. En nuestro caso le pasamos el nombre de la entity
. Como solo tiene una architecture
asociada, no es necesario especificar cuál simular.
Con Modelsim:
$ vlib ms_work
$ vmap work ms_work
$ vcom hello_world.vhd
$ vsim -c hello_world -do 'run -all; quit'
...
# ** Note: Hello world!
# Time: 0 ns Iteration: 0 Instance: /hello_world
...
vlib
, vmap
, vcom
y vsim
son cuatro comandos que proporciona Modelsim. vlib
crea un directorio ( ms_work
) donde se almacenarán los archivos generados. vmap
asocia un directorio creado por vlib
con un nombre lógico ( work
). vcom
compila un archivo fuente VHDL y, de forma predeterminada, almacena el resultado en el directorio asociado al nombre lógico de work
. Finalmente, vsim
simula el programa y produce el mismo tipo de salida que GHDL. Tenga en cuenta una vez más que lo que pide vsim
no es un archivo de origen, sino el nombre de una unidad de compilación ya compilada. La opción -c
le dice al simulador que se ejecute en el modo de línea de comandos en lugar del modo predeterminado de interfaz gráfica de usuario (GUI). La opción -do
se utiliza para pasar una secuencia de comandos TCL para ejecutar después de cargar el diseño. TCL es un lenguaje de script muy utilizado en las herramientas EDA. El valor de la opción -do
puede ser el nombre de un archivo o, como en nuestro ejemplo, una cadena de comandos TCL. run -all; quit
instruye al simulador para que ejecute la simulación hasta que finalice de forma natural, o para siempre si dura para siempre, y luego abandone.
Contador sincrónico
-- File counter.vhd
-- The entity is the interface part. It has a name and a set of input / output
-- ports. Ports have a name, a direction and a type. The bit type has only two
-- values: '0' and '1'. It is one of the standard types.
entity counter is
port(
clock: in bit; -- We are using the rising edge of CLOCK
reset: in bit; -- Synchronous and active HIGH
data: out natural -- The current value of the counter
);
end entity counter;
-- The architecture describes the internals. It is always associated
-- to an entity.
architecture sync of counter is
-- The internal signals we use to count. Natural is another standard
-- type. VHDL is not case sensitive.
signal current_value: natural;
signal NEXT_VALUE: natural;
begin
-- A process is a concurrent statement. It is an infinite loop.
process
begin
-- The wait statement is a synchronization instruction. We wait
-- until clock changes and its new value is '1' (a rising edge).
wait until clock = '1';
-- Our reset is synchronous: we consider it only at rising edges
-- of our clock.
if reset = '1' then
-- <= is the signal assignment operator.
current_value <= 0;
else
current_value <= next_value;
end if;
end process;
-- Another process. The sensitivity list is another way to express
-- synchronization constraints. It (approximately) means: wait until
-- one of the signals in the list changes and then execute the process
-- body. Sensitivity list and wait statements cannot be used together
-- in the same process.
process(current_value)
begin
next_value <= current_value + 1;
end process;
-- A concurrent signal assignment, which is just a shorthand for the
-- (trivial) equivalent process.
data <= current_value;
end architecture sync;
Hola Mundo
Hay muchas formas de imprimir el clásico "¡Hola mundo!" mensaje en VHDL. El más simple de todos es probablemente algo como:
-- File hello_world.vhd
entity hello_world is
end entity hello_world;
architecture arc of hello_world is
begin
assert false report "Hello world!" severity note;
end architecture arc;
Un entorno de simulación para el contador síncrono.
Entornos de simulación
Un entorno de simulación para un diseño VHDL (el diseño bajo prueba o DUT) es otro diseño VHDL que, como mínimo:
- Declara señales correspondientes a los puertos de entrada y salida del DUT.
- Crea una instancia del DUT y conecta sus puertos a las señales declaradas.
- Crea una instancia de los procesos que conducen las señales conectadas a los puertos de entrada del DUT.
Opcionalmente, un entorno de simulación puede instanciar otros diseños que el DUT, como, por ejemplo, generadores de tráfico en interfaces, monitores para verificar protocolos de comunicación, verificadores automáticos de las salidas del DUT ...
El entorno de simulación es analizado, elaborado y ejecutado. La mayoría de los simuladores ofrecen la posibilidad de seleccionar un conjunto de señales para observar, trazar sus formas de onda gráficas, poner puntos de interrupción en el código fuente, paso en el código fuente ...
Idealmente, un entorno de simulación debería ser utilizable como una prueba robusta de no regresión, es decir, debería detectar automáticamente violaciones de las especificaciones del DUT, reportar mensajes de error útiles y garantizar una cobertura razonable de las funciones del DUT. Cuando dichos entornos de simulación están disponibles, se pueden volver a ejecutar en cada cambio del DUT para verificar que aún es funcionalmente correcto, sin la necesidad de inspecciones visuales tediosas y propensas a errores de las trazas de simulación.
En la práctica, diseñar entornos de simulación ideales o incluso buenos es un desafío. Con frecuencia es tan difícil, o incluso más, que el diseño del propio DUT.
En este ejemplo presentamos un entorno de simulación para el ejemplo del contador síncrono . Mostramos cómo ejecutarlo utilizando GHDL y Modelsim y cómo observar formas de onda gráficas utilizando GTKWave con GHDL y el visor de formas de onda integrado con Modelsim. Luego discutimos un aspecto interesante de las simulaciones: ¿cómo detenerlas?
Un primer entorno de simulación para el contador síncrono.
El contador síncrono tiene dos puertos de entrada y uno puertos de salida. Un entorno de simulación muy simple podría ser:
-- File counter_sim.vhd
-- Entities of simulation environments are frequently black boxes without
-- ports.
entity counter_sim is
end entity counter_sim;
architecture sim of counter_sim is
-- One signal per port of the DUT. Signals can have the same name as
-- the corresponding port but they do not need to.
signal clk: bit;
signal rst: bit;
signal data: natural;
begin
-- Instantiation of the DUT
u0: entity work.counter(sync)
port map(
clock => clk,
reset => rst,
data => data
);
-- A clock generating process with a 2ns clock period. The process
-- being an infinite loop, the clock will never stop toggling.
process
begin
clk <= '0';
wait for 1 ns;
clk <= '1';
wait for 1 ns;
end process;
-- The process that handles the reset: active from beginning of
-- simulation until the 5th rising edge of the clock.
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
wait; -- Eternal wait. Stops the process forever.
end process;
end architecture sim;
Simulando con GHDL
Vamos a compilar y simular esto con GHDL:
$ mkdir gh_work
$ ghdl -a --workdir=gh_work counter_sim.vhd
counter_sim.vhd:27:24: unit "counter" not found in 'library "work"'
counter_sim.vhd:50:35: no declaration for "rising_edge"
Entonces los mensajes de error nos dicen dos cosas importantes:
- El analizador GHDL descubrió que nuestro diseño crea una instancia de una entidad llamada
counter
pero esta entidad no se encontró en elwork
biblioteca. Esto es porque no compilamoscounter
antes decounter_sim
. Al compilar diseños VHDL que crean instancias de entidades, los niveles inferiores siempre deben compilarse antes que los niveles superiores (los diseños jerárquicos también pueden compilarse de arriba hacia abajo, pero solo si crean instancias decomponent
, no entidades). - La función
rising_edge
utilizada por nuestro diseño no está definida. Esto se debe al hecho de que esta función se introdujo en VHDL 2008 y no le dijimos a GHDL que usara esta versión del lenguaje (por defecto usa VHDL 1993 con tolerancia de sintaxis de VHDL 1987).
Vamos a corregir los dos errores y lanzar la simulación:
$ ghdl -a --workdir=gh_work --std=08 counter.vhd counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
^C
Tenga en cuenta que la opción --std=08
es necesaria para el análisis y la simulación. Tenga en cuenta también que lanzamos la simulación en la entidad counter_sim
, architecture sim
, no en un archivo fuente.
Como nuestro entorno de simulación tiene un proceso que nunca termina (el proceso que genera el reloj), la simulación no se detiene y debemos interrumpirlo manualmente. En su lugar, podemos especificar un tiempo de parada con la opción --stop-time
:
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns
ghdl:info: simulation stopped by --stop-time
Tal como está, la simulación no nos dice mucho sobre el comportamiento de nuestro DUT. Volcemos los cambios de valor de las señales en un archivo:
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns --vcd=counter_sim.vcd
Vcd.Avhpi_Error!
ghdl:info: simulation stopped by --stop-time
(ignore el mensaje de error, esto es algo que debe solucionarse en GHDL y eso no tiene ninguna consecuencia). Se ha creado un archivo counter_sim.vcd
. Contiene en formato VCD (ASCII) todos los cambios de señal durante la simulación. GTKWave puede mostrarnos las formas de onda gráficas correspondientes:
$ gtkwave counter_sim.vcd
Donde podemos ver que el contador funciona como se espera.
Simulando con Modelsim
El principio es exactamente el mismo con Modelsim:
$ vlib ms_work
...
$ vmap work ms_work
...
$ vcom -nologo -quiet -2008 counter.vhd counter_sim.vhd
$ vsim -voptargs="+acc" 'counter_sim(sim)' -do 'add wave /*; run 60ns'
Tenga en cuenta que la -voptargs="+acc"
pasó a vsim
: impide que el simulador optimice la señal de data
y nos permite verla en las formas de onda.
Con gracia terminando simulaciones
Con ambos simuladores tuvimos que interrumpir la simulación interminable o especificar un tiempo de parada con una opción dedicada. Esto no es muy conveniente. En muchos casos, el tiempo de finalización de una simulación es difícil de anticipar. Sería mucho mejor detener la simulación desde el interior del código VHDL del entorno de simulación, cuando se alcanza una condición particular, como, por ejemplo, cuando el valor actual del contador llega a 20. Esto se puede lograr con una afirmación en el Proceso que maneja el reinicio:
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
loop
wait until rising_edge(clk);
assert data /= 20 report "End of simulation" severity failure;
end loop;
end process;
Mientras los data
sean diferentes de 20, la simulación continúa. Cuando los data
llegan a 20, la simulación se bloquea con un mensaje de error:
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:90:24:@51ns:(assertion failure): End of simulation
ghdl:error: assertion failed
from: process work.counter_sim(sim2).P1 at counter_sim.vhd:90
ghdl:error: simulation failed
Tenga en cuenta que solo hemos vuelto a compilar el entorno de simulación: es el único diseño que ha cambiado y es el nivel superior. Si hubiéramos modificado solo counter.vhd
, habríamos tenido que volver a compilar ambos: counter.vhd
porque cambió y counter_sim.vhd
porque depende de counter.vhd
.
El bloqueo de la simulación con un mensaje de error no es muy elegante. Incluso puede ser un problema cuando se analizan automáticamente los mensajes de simulación para decidir si una prueba automática de no regresión pasó o no. Una solución mejor y mucho más elegante es detener todos los procesos cuando se alcanza una condición. Esto se puede hacer, por ejemplo, agregando una señal boolean
fin de simulación ( eof
). Por defecto, se inicializa en false
al comienzo de la simulación. Uno de nuestros procesos lo establecerá en true
cuando llegue el momento de finalizar la simulación. Todos los demás procesos monitorearán esta señal y se detendrán con una wait
eterna cuando se hará true
:
signal eos: boolean;
...
process
begin
clk <= '0';
wait for 1 ns;
clk <= '1';
wait for 1 ns;
if eos then
report "End of simulation";
wait;
end if;
end process;
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
for i in 1 to 20 loop
wait until rising_edge(clk);
end loop;
eos <= true;
wait;
end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:120:24:@50ns:(report note): End of simulation
Por último, pero no menos importante, hay una solución aún mejor introducida en VHDL 2008 con el paquete estándar env
y los procedimientos de stop
y finish
que declara:
use std.env.all;
...
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
for i in 1 to 20 loop
wait until rising_edge(clk);
end loop;
finish;
end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
simulation finished @49ns
Señales vs. variables, una breve descripción de la semántica de simulación de VHDL
Este ejemplo trata con uno de los aspectos más fundamentales del lenguaje VHDL: la semántica de simulación. Está dirigido a los principiantes en VHDL y presenta una vista simplificada donde se han omitido muchos detalles (procesos pospuestos, interfaz de procedimiento VHDL, variables compartidas ...) Los lectores interesados en la semántica completa real deben consultar el Manual de referencia de idiomas (LRM).
Señales y variables
La mayoría de los lenguajes de programación imperativos clásicos usan variables. Son contenedores de valor. Un operador de asignación se utiliza para almacenar un valor en una variable:
a = 15;
y el valor actualmente almacenado en una variable se puede leer y usar en otras declaraciones:
if(a == 15) { print "Fifteen" }
VHDL también usa variables y tienen exactamente el mismo rol que en la mayoría de los idiomas imperativos. Pero VHDL también ofrece otro tipo de contenedor de valor: la señal. Las señales también almacenan valores, también se pueden asignar y leer. El tipo de valores que se pueden almacenar en señales es (casi) el mismo que en las variables.
Entonces, ¿por qué tener dos tipos de contenedores de valor? La respuesta a esta pregunta es esencial y está en el corazón de la lengua. Comprender la diferencia entre variables y señales es lo primero que debe hacer antes de intentar programar algo en VHDL.
Ilustremos esta diferencia en un ejemplo concreto: el intercambio.
Nota: todos los siguientes fragmentos de código son parte de procesos. Más adelante veremos qué son los procesos.
tmp := a;
a := b;
b := tmp;
Swaps variables a
y b
. Después de ejecutar estas 3 instrucciones, el nuevo contenido de a
es el contenido antiguo de b
y viceversa. Como en la mayoría de los lenguajes de programación, se necesita una tercera variable temporal ( tmp
). Si, en lugar de variables, quisiéramos intercambiar señales, escribiríamos:
r <= s;
s <= r;
o:
s <= r;
r <= s;
¡Con el mismo resultado y sin la necesidad de una tercera señal temporal!
Nota: el operador de asignación de señal VHDL
<=
es diferente del operador de asignación de variable:=
.
Veamos un segundo ejemplo en el que suponemos que el subprograma de print
imprime la representación decimal de su parámetro. Si a
es una variable entera y su valor actual es 15, ejecutando:
a := 2 * a;
a := a - 5;
a := a / 5;
print(a);
imprimirá:
5
Si ejecutamos este paso a paso en un depurador, podemos ver el valor de a
cambio de los iniciales 15 a 30, 25 y finalmente 5.
Pero si s
es una señal entera y su valor actual es 15, ejecutando:
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
imprimirá:
15
3
Si ejecutamos este paso a paso en un depurador, no veremos ningún cambio de valor de s
hasta después de la instrucción de wait
. ¡Además, el valor final de s
no será 15, 30, 25 o 5 sino 3!
Este comportamiento aparentemente extraño se debe a la naturaleza fundamentalmente paralela del hardware digital, como veremos en las siguientes secciones.
Paralelismo
VHDL es un lenguaje de descripción de hardware (HDL), es paralelo por naturaleza. Un programa VHDL es una colección de programas secuenciales que se ejecutan en paralelo. Estos programas secuenciales se denominan procesos:
P1: process
begin
instruction1;
instruction2;
...
instructionN;
end process P1;
P2: process
begin
...
end process P2;
Los procesos, al igual que el hardware que están modelando, nunca terminan: son bucles infinitos. Después de ejecutar la última instrucción, la ejecución continúa con la primera.
Al igual que con cualquier lenguaje de programación que admite una forma u otra de paralelismo, un programador es responsable de decidir qué proceso ejecutar (y cuándo) durante una simulación VHDL. Además, el lenguaje ofrece construcciones específicas para la comunicación y la sincronización entre procesos.
Programación
El programador mantiene una lista de todos los procesos y, para cada uno de ellos, registra su estado actual que se puede running
, run-able
o suspended
. Hay como máximo un proceso en estado de running
: el que se ejecuta actualmente. Mientras el proceso actualmente en ejecución no ejecute una instrucción de wait
, continúa ejecutándose e impide que se ejecute cualquier otro proceso. El programador de VHDL no es preventivo: es responsabilidad de cada proceso suspenderse y dejar que se ejecuten otros procesos. Este es uno de los problemas que los principiantes en VHDL encuentran con frecuencia: el proceso de ejecución libre.
P3: process
variable a: integer;
begin
a := s;
a := 2 * a;
r <= a;
end process P3;
Nota: variable de
a
se declara localmente mientras que las señaless
yr
son declarados en otro lugar, en un nivel superior. Las variables VHDL son locales al proceso que las declara y no pueden ser vistas por otros procesos. Otro proceso también podría declarar una variable llamadaa
, no sería la misma variable que la del procesoP3
.
Tan pronto como el programador reanude el proceso P3
, la simulación se atascará, la hora actual de la simulación no progresará más y la única forma de detener esto será matar o interrumpir la simulación. La razón es que P3
no tiene una declaración de wait
y, por lo tanto, permanecerá en estado de running
para siempre, repitiendo sus 3 instrucciones. Ningún otro proceso tendrá la oportunidad de ejecutarse, incluso si es run-able
.
Incluso los procesos que contienen una instrucción de wait
pueden causar el mismo problema:
P4: process
variable a: integer;
begin
a := s;
a := 2 * a;
if a = 16 then
wait on s;
end if;
r <= a;
end process P4;
Nota: el operador de igualdad VHDL es
=
.
Si el proceso P4
se reanuda mientras el valor de la señal s
es 3, se ejecutará para siempre porque la condición a a = 16
nunca será verdadera.
Supongamos que nuestro programa VHDL no contiene tales procesos patológicos. Cuando el proceso en ejecución ejecuta una instrucción de wait
, se suspende inmediatamente y el programador la pone en el estado suspended
. La instrucción de wait
también conlleva la condición de que el proceso pueda volver a run-able
. Ejemplo:
wait on s;
Me significa suspender hasta que el valor de la señal s
cambios. Esta condición es registrada por el planificador. El programador luego selecciona otro proceso entre los run-able
, lo pone en estado de running
y lo ejecuta. Y lo mismo se repite hasta que todos run-able
procesos ejecutables se hayan ejecutado y suspendido.
Nota importante: cuando se pueden
run-able
varios procesos, el estándar VHDL no especifica cómo el programador seleccionará cuál ejecutar. Una consecuencia es que, dependiendo del simulador, la versión del simulador, el sistema operativo o cualquier otra cosa, dos simulaciones del mismo modelo VHDL podrían, en un momento dado, hacer diferentes elecciones y seleccionar un proceso diferente para ejecutar. Si esta elección tuviera un impacto en los resultados de la simulación, podríamos decir que VHDL no es determinista. Como el no determinismo es usualmente indeseable, sería responsabilidad de los programadores evitar situaciones no deterministas. Afortunadamente, VHDL se encarga de esto y es aquí donde las señales entran en escena.
Señales y comunicación entre procesos.
VHDL evita el no determinismo utilizando dos características específicas:
- Los procesos pueden intercambiar información solo a través de señales.
signal r, s: integer; -- Common to all processes
...
P5: process
variable a: integer; -- Different from variable a of process P6
begin
a := s + 1;
r <= a;
a := r + 1;
wait on s;
end process P5;
P6: process
variable a: integer; -- Different from variable a of process P5
begin
a := r + 1;
s <= a;
wait on r;
end process P6;
Nota: los comentarios de VHDL se extienden desde
--
hasta el final de la línea.
- El valor de una señal VHDL no cambia durante la ejecución de procesos
Cada vez que se asigna una señal, el programador registra el valor asignado, pero el valor actual de la señal permanece sin cambios. Esta es otra diferencia importante con las variables que toman su nuevo valor inmediatamente después de ser asignadas.
Veamos una ejecución del proceso P5
anterior y supongamos que a=5
, s=1
r=0
cuando el planificador lo reanuda. Después de ejecutar la instrucción a := s + 1;
, el valor de la variable a
cambia y se convierte en 2 (1 + 1). Al ejecutar la siguiente instrucción r <= a;
es el nuevo valor de a
(2) que se asigna a r
. Pero siendo r
una señal, el valor actual de r
sigue siendo 0. Entonces, cuando se ejecuta a := r + 1;
, la variable a
toma (inmediatamente) el valor 1 (0 + 1), no 3 (2 + 1) como diría la intuición.
¿Cuándo la señal r
tomará realmente su nuevo valor? Cuando el programador haya ejecutado todos los procesos ejecutables y todos se suspenderán. Esto también se conoce como: después de un ciclo delta . Solo entonces el programador verá todos los valores que se han asignado a las señales y actualizará realmente los valores de las señales. Una simulación VHDL es una alternancia de fases de ejecución y fases de actualización de señal. Durante las fases de ejecución, el valor de las señales se congela. Simbólicamente, decimos que entre una fase de ejecución y la siguiente fase de actualización de señal transcurrió un delta de tiempo. Esto no es en tiempo real. Un ciclo delta no tiene duración física.
Gracias a este mecanismo de actualización de señal retardada, VHDL es determinista. Los procesos pueden comunicarse solo con señales y las señales no cambian durante la ejecución de los procesos. Por lo tanto, el orden de ejecución de los procesos no importa: su entorno externo (las señales) no cambia durante la ejecución. P5.a=5
esto en el ejemplo anterior con los procesos P5
y P6
, donde el estado inicial es P5.a=5
, P6.a=10
, s=17
, r=0
y donde el programador decide ejecutar P5
primero y P6
continuación . La siguiente tabla muestra el valor de las dos variables, los valores actuales y siguientes de las señales después de ejecutar cada instrucción de cada proceso:
proceso / instrucción | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Estado inicial | 5 | 10 | 17 | 0 | ||
P5 / a := s + 1 | 18 | 10 | 17 | 0 | ||
P5 / r <= a | 18 | 10 | 17 | 0 | 18 | |
P5 / a := r + 1 | 1 | 10 | 17 | 0 | 18 | |
P5 / wait on s | 1 | 10 | 17 | 0 | 18 | |
P6 / a := r + 1 | 1 | 1 | 17 | 0 | 18 | |
P6 / s <= a | 1 | 1 | 17 | 1 | 0 | 18 |
P6 / wait on r | 1 | 1 | 17 | 1 | 0 | 18 |
Después de la actualización de la señal | 1 | 1 | 1 | 18 |
Con las mismas condiciones iniciales, si el programador decide ejecutar P6
primero y P5
continuación:
proceso / instrucción | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Estado inicial | 5 | 10 | 17 | 0 | ||
P6 / a := r + 1 | 5 | 1 | 17 | 0 | ||
P6 / s <= a | 5 | 1 | 17 | 1 | 0 | |
P6 / wait on r | 5 | 1 | 17 | 1 | 0 | |
P5 / a := s + 1 | 18 | 1 | 17 | 1 | 0 | |
P5 / r <= a | 18 | 1 | 17 | 1 | 0 | 18 |
P5 / a := r + 1 | 1 | 1 | 17 | 1 | 0 | 18 |
P5 / wait on s | 1 | 1 | 17 | 1 | 0 | 18 |
Después de la actualización de la señal | 1 | 1 | 1 | 18 |
Como podemos ver, después de la ejecución de nuestros dos procesos, el resultado es el mismo, independientemente del orden de ejecución.
Esta semántica de asignación de señal contraintuitiva es la causa de un segundo tipo de problemas que los principiantes de VHDL encuentran con frecuencia: la asignación que aparentemente no funciona porque se retrasa un ciclo delta. Cuando se ejecuta el proceso P5
paso a paso en un depurador, después de asignar r
18 y a a
r + 1
, se puede esperar que el valor de a
sea 19, pero el depurador dice obstinadamente que r=0
y a=1
...
Nota: la misma señal se puede asignar varias veces durante la misma fase de ejecución. En este caso, es la última asignación la que decide el siguiente valor de la señal. Las otras asignaciones no tienen ningún efecto, como si nunca hubieran sido ejecutadas.
Es hora de verificar nuestra comprensión: vuelva a nuestro primer ejemplo de intercambio y trate de entender por qué:
process
begin
---
s <= r;
r <= s;
---
end process;
en realidad intercambia las señales r
y s
sin la necesidad de una tercera señal temporal y por qué:
process
begin
---
r <= s;
s <= r;
---
end process;
sería estrictamente equivalente. Trate de entender también por qué, si s
es una señal entera y su valor actual es 15, y ejecutamos:
process
begin
---
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
---
end process;
las dos primeras asignaciones de la señal s
no tienen efecto, por qué s
se asigna finalmente 3 y por qué los dos valores impresos son 15 y 3.
Tiempo fisico
Para modelar hardware es muy útil poder modelar el tiempo físico tomado por alguna operación. Aquí hay un ejemplo de cómo se puede hacer esto en VHDL. El ejemplo modela un contador síncrono y es un código VHDL completo y autónomo que se puede compilar y simular:
-- File counter.vhd
entity counter is
end entity counter;
architecture arc of counter is
signal clk: bit; -- Type bit has two values: '0' and '1'
signal c, nc: natural; -- Natural (non-negative) integers
begin
P1: process
begin
clk <= '0';
wait for 10 ns; -- Ten nano-seconds delay
clk <= '1';
wait for 10 ns; -- Ten nano-seconds delay
end process P1;
P2: process
begin
if clk = '1' and clk'event then
c <= nc;
end if;
wait on clk;
end process P2;
P3: process
begin
nc <= c + 1 after 5 ns; -- Five nano-seconds delay
wait on c;
end process P3;
end architecture arc;
En el proceso P1
la instrucción de wait
no se utiliza para esperar hasta que cambie el valor de una señal, como vimos hasta ahora, sino para esperar una duración determinada. Este proceso modela un generador de reloj. Signal clk
es el reloj de nuestro sistema, es periódico con un período de 20 ns (50 MHz) y tiene un ciclo de trabajo.
Proceso P2
modelos un registro que, si un borde ascendente de clk
acaba de ocurrir, asigna el valor de su entrada nc
a su salida c
y luego espera para el siguiente cambio de valor de clk
.
El proceso P3
modela un incrementador que asigna el valor de su entrada c
, incrementado en uno, a su salida nc
... con un retardo físico de 5 ns. Entonces espera hasta que cambie el valor de su entrada c
. Esto también es nuevo. Hasta ahora siempre asignamos señales con:
s <= value;
que, por las razones explicadas en las secciones anteriores, podemos traducir implícitamente a:
s <= value; -- after delta
Este pequeño sistema de hardware digital podría estar representado por la siguiente figura:
Con la introducción del tiempo físico, y sabiendo que también tenemos un tiempo simbólico medido en delta , ahora tenemos un tiempo bidimensional que denotaremos T+D
donde T
es un tiempo físico medido en nano-segundos y D
un número de deltas (sin duración física).
La imagen completa
Hay un aspecto importante de la simulación VHDL que aún no discutimos: después de una fase de ejecución, todos los procesos están en estado suspended
. Informamos de manera informal que el programador luego actualiza los valores de las señales que se han asignado. Pero, en nuestro ejemplo de un contador síncrono, ¿actualizará las señales clk
, c
y nc
al mismo tiempo? ¿Qué pasa con los retrasos físicos? ¿Y qué sucede después con todos los procesos en estado suspended
y ninguno en estado run-able
?
El algoritmo de simulación completo (pero simplificado) es el siguiente:
- Inicialización
- Establezca la hora actual
Tc
en 0 + 0 (0 ns, 0 delta-ciclo) - Inicializa todas las señales.
- Ejecute cada proceso hasta que se suspenda en una instrucción de
wait
.- Registrar los valores y retrasos de las asignaciones de señales.
- Registre las condiciones para que se reanude el proceso (retraso o cambio de señal).
- Calcule la próxima vez
Tn
como la primera de:- El tiempo de reanudación de los procesos suspendidos por una
wait for <delay>
. - La próxima vez que cambie un valor de señal.
- El tiempo de reanudación de los procesos suspendidos por una
- Establezca la hora actual
- Ciclo de simulacion
-
Tc=Tn
. - Actualizar las señales que deben ser.
- Ponga en estado de
run-able
todos los procesos que esperaban un cambio de valor de una de las señales que se ha actualizado. - Ponga en estado de
run-able
todos los procesos que fueron suspendidos por una instrucción dewait for <delay>
y cuyo tiempo de reanudación esTc
. - Ejecutar todos los procesos ejecutables hasta que se suspendan.
- Registrar los valores y retrasos de las asignaciones de señales.
- Registre las condiciones para que se reanude el proceso (retraso o cambio de señal).
- Calcule la próxima vez
Tn
como la primera de:- El tiempo de reanudación de los procesos suspendidos por una
wait for <delay>
. - La próxima vez que cambie un valor de señal.
- El tiempo de reanudación de los procesos suspendidos por una
- Si
Tn
es infinito, detén la simulación. Si no, comienza un nuevo ciclo de simulación.
-
Simulación manual
Para concluir, ahora ejerzamos manualmente el algoritmo de simulación simplificado en el contador síncrono presentado anteriormente. Decidimos arbitrariamente que, cuando se ejecutan varios procesos, el orden será P3
> P2
> P1
. Las siguientes tablas representan la evolución del estado del sistema durante la inicialización y los primeros ciclos de simulación. Cada señal tiene su propia columna en la que se indica el valor actual. Cuando se ejecuta una asignación de señal, el valor programado se agrega al valor actual, por ejemplo, a/b@T+D
si el valor actual es a
y el siguiente valor será b
en el tiempo T+D
(tiempo físico más ciclos delta) . Las 3 últimas columnas indican la condición para reanudar los procesos suspendidos (nombre de las señales que deben cambiar o la hora a la que se debe reanudar el proceso).
Fase de inicialización:
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 0 + 0 | |||||||
Inicializar todas las señales | 0 + 0 | '0' | 0 | 0 | ||||
P3/nc<=c+1 after 5 ns | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | ||||
P3/wait on c | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | c | |||
P2/if clk='1'... | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | c | |||
P2/end if | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | c | |||
P2/wait on clk | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | clk | c | ||
P1/clk<='0' | 0 + 0 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | clk | c | ||
P1/wait for 10 ns | 0 + 0 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Calcular la próxima vez | 0 + 0 | 0 + 1 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c |
Ciclo de simulación # 1
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 0 + 1 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Actualizar señales | 0 + 1 | '0' | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Calcular la próxima vez | 0 + 1 | 5 + 0 | '0' | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c |
Nota: durante el primer ciclo de simulación no hay fase de ejecución porque ninguno de nuestros 3 procesos cumple con su condición de reanudación.
P2
está esperando un cambio de valor declk
y ha habido una transacción enclk
, pero como los valores antiguos y nuevos son los mismos, esto no es un cambio de valor.
Ciclo de simulación # 2
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 5 + 0 | '0' | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Actualizar señales | 5 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c | |
Calcular la próxima vez | 5 + 0 | 10 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c |
Nota: de nuevo, no hay fase de ejecución.
nc
cambió pero ningún proceso está esperando ennc
.
Ciclo de simulación # 3
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 10 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c | |
Actualizar señales | 10 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c | |
P1/clk<='1' | 10 + 0 | '0' / '1' @ 10 + 1 | 0 | 1 | clk | c | ||
P1/wait for 10 ns | 10 + 0 | '0' / '1' @ 10 + 1 | 0 | 1 | 20 + 0 | clk | c | |
Calcular la próxima vez | 10 + 0 | 10 + 1 | '0' / '1' @ 10 + 1 | 0 | 1 | 20 + 0 | clk | c |
Ciclo de simulación # 4
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 10 + 1 | '0' / '1' @ 10 + 1 | 0 | 1 | 20 + 0 | clk | c | |
Actualizar señales | 10 + 1 | '1' | 0 | 1 | 20 + 0 | clk | c | |
P2/if clk='1'... | 10 + 1 | '1' | 0 | 1 | 20 + 0 | c | ||
P2/c<=nc | 10 + 1 | '1' | 0/1 @ 10 + 2 | 1 | 20 + 0 | c | ||
P2/end if | 10 + 1 | '1' | 0/1 @ 10 + 2 | 1 | 20 + 0 | c | ||
P2/wait on clk | 10 + 1 | '1' | 0/1 @ 10 + 2 | 1 | 20 + 0 | clk | c | |
Calcular la próxima vez | 10 + 1 | 10 + 2 | '1' | 0/1 @ 10 + 2 | 1 | 20 + 0 | clk | c |
Ciclo de simulación # 5
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 10 + 2 | '1' | 0/1 @ 10 + 2 | 1 | 20 + 0 | clk | c | |
Actualizar señales | 10 + 2 | '1' | 1 | 1 | 20 + 0 | clk | c | |
P3/nc<=c+1 after 5 ns | 10 + 2 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | ||
P3/wait on c | 10 + 2 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c | |
Calcular la próxima vez | 10 + 2 | 15 + 0 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c |
Nota: se podría pensar que la actualización de
nc
se programaría en15+2
, mientras que la programamos en15+0
. Cuando se agrega un retardo físico distinto de cero (aquí5 ns
) a una hora actual (10+2
), los ciclos delta se desvanecen. De hecho, los ciclos delta son útiles solo para distinguir diferentes tiempos de simulaciónT+0
,T+1
... con el mismo tiempo físicoT
Tan pronto como el tiempo físico cambie, los ciclos delta se pueden restablecer.
Ciclo de simulación # 6
Operaciones | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Establecer hora actual | 15 + 0 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c | |
Actualizar señales | 15 + 0 | '1' | 1 | 2 | 20 + 0 | clk | c | |
Calcular la próxima vez | 15 + 0 | 20 + 0 | '1' | 1 | 2 | 20 + 0 | clk | c |
Nota: de nuevo, no hay fase de ejecución.
nc
cambió pero ningún proceso está esperando ennc
.